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 = isAdmin ? '' : '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', 'tekniikka', '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 === 'tekniikka') { loadDevices(); loadSitesTab(); loadIpam(); }
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) ||
(l.vlan || '').toLowerCase().includes(query) ||
(l.laite || '').toLowerCase().includes(query) ||
(l.portti || '').toLowerCase().includes(query) ||
(l.ip || '').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}
` : ''}
Nopeus
${detailVal(l.liittymanopeus)}
Hinta / kk
${formatPrice(l.hinta)}
Sopimuskausi
${l.sopimuskausi ? l.sopimuskausi + ' kk' : '-'}
Alkaen
${detailVal(l.alkupvm)}
Laite
${detailVal(l.laite)}
Portti
${detailVal(l.portti)}
`;
}).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)}
` : ''}
`;
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 => ``).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 = `
`;
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,
vlan: row.querySelector('.l-vlan').value,
laite: row.querySelector('.l-laite').value,
portti: row.querySelector('.l-portti').value,
ip: row.querySelector('.l-ip').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';
// Piilota superadmin-vaihtoehto ellei ole superadmin
const saOption = document.querySelector('#user-form-role option[value="superadmin"]');
if (saOption) saOption.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// Piilota yrityscheckboxit adminilta (näkee vain oman yrityksen)
const compSection = document.getElementById('user-company-checkboxes')?.closest('.form-group');
if (compSection) compSection.style.display = currentUser?.role === 'superadmin' ? '' : 'none';
// 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); }
});
// ==================== OMA PROFIILI ====================
const profileModal = document.getElementById('profile-modal');
document.getElementById('btn-profile').addEventListener('click', openProfileModal);
document.getElementById('user-info').addEventListener('click', openProfileModal);
document.getElementById('profile-modal-close').addEventListener('click', () => profileModal.style.display = 'none');
document.getElementById('profile-form-cancel').addEventListener('click', () => profileModal.style.display = 'none');
async function openProfileModal() {
// Hae tuoreet tiedot
const auth = await apiCall('check_auth');
if (!auth.authenticated) return;
document.getElementById('profile-username').value = auth.username;
document.getElementById('profile-nimi').value = auth.nimi || '';
document.getElementById('profile-email').value = auth.email || '';
document.getElementById('profile-password').value = '';
// Allekirjoitukset
const sigSection = document.getElementById('profile-signatures-section');
const sigList = document.getElementById('profile-signatures-list');
const userSigs = auth.signatures || {};
try {
const mailboxes = await apiCall('all_mailboxes');
if (mailboxes.length === 0) {
sigSection.style.display = 'none';
} else {
sigSection.style.display = '';
sigList.innerHTML = mailboxes.map(mb =>
`
`
).join('');
}
} catch {
sigSection.style.display = 'none';
}
profileModal.style.display = 'flex';
}
document.getElementById('profile-form').addEventListener('submit', async (e) => {
e.preventDefault();
const signatures = {};
document.querySelectorAll('.profile-sig-textarea').forEach(ta => {
const mbId = ta.dataset.mailboxId;
const val = ta.value.trim();
if (val) signatures[mbId] = val;
});
const data = {
nimi: document.getElementById('profile-nimi').value,
email: document.getElementById('profile-email').value,
signatures,
};
const pw = document.getElementById('profile-password').value;
if (pw) data.password = pw;
try {
await apiCall('profile_update', 'POST', data);
// Päivitä UI
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 || {};
document.getElementById('user-info').textContent = auth.nimi || auth.username;
}
profileModal.style.display = 'none';
alert('Profiili päivitetty!');
} 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 + automaattinen tunnistus
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);
});
// Automaattinen asiakastunnistus sähköpostin perusteella
const senderEmail = (ticket.from_email || '').toLowerCase().trim();
const senderLine = document.getElementById('ticket-sender-line');
if (senderEmail && !ticket.customer_id) {
const matchedCustomer = customers.find(c =>
(c.sahkoposti || '').toLowerCase().trim() === senderEmail ||
(c.laskutussahkoposti || '').toLowerCase().trim() === senderEmail
);
if (matchedCustomer) {
// Löytyi asiakas → linkitä automaattisesti
custSelect.value = matchedCustomer.id;
custSelect.dispatchEvent(new Event('change'));
if (senderLine) {
senderLine.insertAdjacentHTML('beforeend',
` ✓ ${esc(matchedCustomer.yritys)}`);
}
} else {
// Ei löytynyt → näytä "Lisää liidi" -nappi
if (senderLine) {
senderLine.insertAdjacentHTML('beforeend',
` `);
document.getElementById('btn-ticket-add-lead')?.addEventListener('click', () => {
// Avaa liidilomake esitäytetyillä tiedoilla
openLeadForm(null);
document.getElementById('lead-form-yhteyshenkilo').value = ticket.from_name || '';
document.getElementById('lead-form-sahkoposti').value = ticket.from_email || '';
document.getElementById('lead-form-muistiinpanot').value = 'Tiketti: ' + (ticket.subject || '') + '\\nLähettäjä: ' + (ticket.from_email || '');
});
}
}
} else if (senderEmail && ticket.customer_id) {
// Asiakas jo linkitetty — näytä badge
const linked = customers.find(c => c.id === ticket.customer_id);
if (linked && senderLine) {
senderLine.insertAdjacentHTML('beforeend',
` ✓ ${esc(linked.yritys)}`);
}
}
} 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 ``;
}).join('');
// Show detail, hide list + other views
document.getElementById('ticket-list-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-templates-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';
// TO-kenttä — tiketin alkuperäinen lähettäjä
const toField = document.getElementById('reply-to');
if (toField) toField.value = ticket.from_email || '';
// 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 useSigCheck = document.getElementById('reply-use-signature');
const sig = currentUserSignatures[mbId] || '';
if (sig && useSigCheck && useSigCheck.checked) {
sigPreview.textContent = '-- \n' + sig;
sigPreview.style.display = 'block';
} else {
sigPreview.style.display = 'none';
}
}
updateSignaturePreview(ticket.mailbox_id || '');
// Allekirjoitus-checkbox: päivitä esikatselu vaihdettaessa
const useSigCheckbox = document.getElementById('reply-use-signature');
if (useSigCheckbox) {
useSigCheckbox.addEventListener('change', () => {
const mbSelect = document.getElementById('reply-mailbox-select');
updateSignaturePreview(mbSelect ? mbSelect.value : '');
});
}
// 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-templates-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');
const useSigEl = document.getElementById('reply-use-signature');
const sigLabel = useSigEl ? useSigEl.closest('label') : null;
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';
if (sigLabel) sigLabel.style.display = 'none';
} else {
textarea.placeholder = 'Kirjoita vastaus...';
sendBtn.textContent = 'Lähetä vastaus';
if (metaFields) metaFields.style.display = '';
if (tplWrap) tplWrap.style.display = '';
if (sigLabel) sigLabel.style.display = '';
// Näytä allekirjoitus jos checkbox päällä
if (sigPrev.textContent.trim() && useSigEl && useSigEl.checked) 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 toFld = document.getElementById('reply-to');
const ccFld = document.getElementById('reply-cc');
const useSig = document.getElementById('reply-use-signature');
if (mbSel) payload.mailbox_id = mbSel.value;
if (toFld && toFld.value.trim()) payload.to = toFld.value.trim();
if (ccFld) payload.cc = ccFld.value.trim();
if (useSig && !useSig.checked) payload.no_signature = true;
}
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-templates-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); }
}
// ==================== VASTAUSPOHJAT (TUKITABISSA) ====================
function showTemplatesView() {
document.getElementById('ticket-list-view').style.display = 'none';
document.getElementById('ticket-detail-view').style.display = 'none';
document.getElementById('ticket-rules-view').style.display = 'none';
document.getElementById('ticket-templates-view').style.display = 'block';
hideTplForm();
renderTplList();
}
function hideTemplatesView() {
document.getElementById('ticket-templates-view').style.display = 'none';
document.getElementById('ticket-list-view').style.display = 'block';
}
function renderTplList() {
const list = document.getElementById('tpl-list');
if (!list) return;
if (replyTemplates.length === 0) {
list.innerHTML = 'Ei vastauspohjia vielä. Lisää ensimmäinen klikkaamalla "+ Lisää pohja".
';
return;
}
list.innerHTML = replyTemplates.map(t =>
`
${esc(t.nimi)}
${esc(t.body.substring(0, 100))}
`
).join('');
}
function showTplForm(tpl) {
document.getElementById('tpl-form-container').style.display = '';
document.getElementById('tpl-form-title').textContent = tpl ? 'Muokkaa vastauspohjaa' : 'Uusi vastauspohja';
document.getElementById('tpl-form-id').value = tpl ? tpl.id : '';
document.getElementById('tpl-form-name').value = tpl ? tpl.nimi : '';
document.getElementById('tpl-form-body').value = tpl ? tpl.body : '';
}
function hideTplForm() {
document.getElementById('tpl-form-container').style.display = 'none';
}
document.getElementById('btn-ticket-templates').addEventListener('click', async () => {
await loadTemplates();
showTemplatesView();
});
document.getElementById('btn-templates-back').addEventListener('click', () => hideTemplatesView());
document.getElementById('btn-add-tpl').addEventListener('click', () => showTplForm(null));
document.getElementById('btn-cancel-tpl').addEventListener('click', () => hideTplForm());
document.getElementById('btn-save-tpl').addEventListener('click', async () => {
const nimi = document.getElementById('tpl-form-name').value.trim();
const body = document.getElementById('tpl-form-body').value.trim();
if (!nimi || !body) { alert('Täytä nimi ja sisältö'); return; }
const id = document.getElementById('tpl-form-id').value || undefined;
try {
await apiCall('reply_template_save', 'POST', { id, nimi, body });
hideTplForm();
await loadTemplates();
renderTplList();
} catch (e) { alert(e.message); }
});
window.editTpl = function(id) {
const t = replyTemplates.find(x => x.id === id);
if (t) showTplForm(t);
};
window.deleteTpl = async function(id) {
if (!confirm('Poistetaanko vastauspohja?')) return;
try {
await apiCall('reply_template_delete', 'POST', { id });
await loadTemplates();
renderTplList();
} 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=Esimerkkikatu+1&postinumero=00100&kaupunki=Helsinki`;
// 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=Esimerkkikatu+1&postinumero=00100&kaupunki=Helsinki`;
} 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 (yhteensopivuus: vanha 'devices' → 'tekniikka')
let enabledMods = comp?.enabled_modules || [];
if (enabledMods.includes('devices') && !enabledMods.includes('tekniikka')) {
enabledMods = enabledMods.map(m => m === 'devices' ? 'tekniikka' : m);
}
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());
// ==================== TEKNIIKKA SUB-TABS ====================
function switchSubTab(target) {
document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('#tab-content-tekniikka .sub-tab-content').forEach(c => c.classList.remove('active'));
const btn = document.querySelector(`.sub-tab[data-subtab="${target}"]`);
if (btn) btn.classList.add('active');
const content = document.getElementById('subtab-' + target);
if (content) content.classList.add('active');
}
document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(btn => {
btn.addEventListener('click', () => switchSubTab(btn.dataset.subtab));
});
// ==================== SIJAINNIT (SITES) — TEKNIIKKA TAB ====================
let sitesTabData = [];
async function loadSitesTab() {
try {
sitesData = await apiCall('sites');
sitesTabData = sitesData;
renderSitesTab();
renderSitesSettings(); // Päivitä myös asetuksissa
} catch (e) { console.error(e); }
}
function renderSitesTab() {
const query = (document.getElementById('site-search-input')?.value || '').toLowerCase().trim();
let filtered = sitesTabData;
if (query) {
filtered = sitesTabData.filter(s =>
(s.nimi || '').toLowerCase().includes(query) ||
(s.osoite || '').toLowerCase().includes(query) ||
(s.kaupunki || '').toLowerCase().includes(query)
);
}
const tbody = document.getElementById('site-tbody');
const noSites = document.getElementById('no-sites-tab');
if (filtered.length === 0) {
tbody.innerHTML = '';
if (noSites) noSites.style.display = 'block';
} else {
if (noSites) noSites.style.display = 'none';
tbody.innerHTML = filtered.map(s => {
const deviceCount = devicesData.filter(d => d.site_id === s.id).length;
return `
| ${esc(s.nimi)} |
${esc(s.osoite || '-')} |
${esc(s.kaupunki || '-')} |
${deviceCount} |
|
`;
}).join('');
}
document.getElementById('site-count').textContent = filtered.length + ' sijaintia';
}
function editSiteTab(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 = '';
}
// Alias vanhalle editSite-funktiolle
function editSite(id) { editSiteTab(id); }
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 });
loadSitesTab();
loadDevices();
} catch (e) { alert(e.message); }
}
// Renderöi sijainnit myös asetuksissa (company detail)
function renderSitesSettings() {
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('');
}
// Alias loadSites asetuksista kutsuun
async function loadSites() { await loadSitesTab(); }
function renderSites() { renderSitesSettings(); }
document.getElementById('site-search-input')?.addEventListener('input', () => renderSitesTab());
// Lisää sijainti -napit (tekniikka-tab + laitteet-sivun quick-nappi)
document.getElementById('btn-add-site-tab')?.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 = '';
switchSubTab('sites');
});
document.getElementById('btn-add-site-quick')?.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 = '';
switchSubTab('sites');
});
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';
loadSitesTab();
} catch (e) { alert(e.message); }
});
document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
document.getElementById('site-form-container').style.display = 'none';
});
// ==================== IPAM ====================
let ipamData = [];
async function loadIpam() {
try {
ipamData = await apiCall('ipam');
renderIpam();
} catch (e) { console.error(e); }
}
function renderIpam() {
const query = (document.getElementById('ipam-search-input')?.value || '').toLowerCase().trim();
let filtered = ipamData;
if (query) {
filtered = ipamData.filter(e =>
(e.tyyppi || '').toLowerCase().includes(query) ||
(e.verkko || '').toLowerCase().includes(query) ||
(e.nimi || '').toLowerCase().includes(query) ||
(e.site_name || '').toLowerCase().includes(query) ||
(e.asiakas || '').toLowerCase().includes(query) ||
(e.lisatiedot || '').toLowerCase().includes(query) ||
String(e.vlan_id || '').includes(query)
);
}
const tbody = document.getElementById('ipam-tbody');
const noIpam = document.getElementById('no-ipam');
if (filtered.length === 0) {
tbody.innerHTML = '';
if (noIpam) noIpam.style.display = 'block';
} else {
if (noIpam) noIpam.style.display = 'none';
const tyyppiLabel = { subnet: 'Subnet', vlan: 'VLAN', ip: 'IP' };
const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
tbody.innerHTML = filtered.map(e => `
| ${tyyppiLabel[e.tyyppi] || e.tyyppi} |
${esc(e.verkko || '-')} |
${e.vlan_id ? '' + e.vlan_id + '' : '-'} |
${esc(e.nimi || '-')} |
${e.site_name ? esc(e.site_name) : '-'} |
${tilaLabel[e.tila] || e.tila} |
${esc(e.asiakas || '-')} |
|
`).join('');
}
document.getElementById('ipam-count').textContent = filtered.length + ' merkintää' + (query ? ` (${ipamData.length} yhteensä)` : '');
}
async function loadIpamSitesDropdown() {
try {
if (!sitesData || sitesData.length === 0) sitesData = await apiCall('sites');
const sel = document.getElementById('ipam-form-site');
sel.innerHTML = '' +
sitesData.map(s => ``).join('');
} catch (e) { console.error(e); }
}
async function editIpam(id) {
const e = ipamData.find(x => x.id === id);
if (!e) return;
document.getElementById('ipam-form-id').value = e.id;
document.getElementById('ipam-form-tyyppi').value = e.tyyppi || 'ip';
document.getElementById('ipam-form-verkko').value = e.verkko || '';
document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
document.getElementById('ipam-form-nimi').value = e.nimi || '';
document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
document.getElementById('ipam-form-asiakas').value = e.asiakas || '';
document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
await loadIpamSitesDropdown();
document.getElementById('ipam-form-site').value = e.site_id || '';
document.getElementById('ipam-modal-title').textContent = 'Muokkaa IPAM-merkintää';
document.getElementById('ipam-modal').style.display = 'flex';
}
async function deleteIpam(id, name) {
if (!confirm(`Poistetaanko IPAM-merkintä "${name}"?`)) return;
try {
await apiCall('ipam_delete', 'POST', { id });
loadIpam();
} catch (e) { alert(e.message); }
}
document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
document.getElementById('ipam-form-id').value = '';
document.getElementById('ipam-form').reset();
await loadIpamSitesDropdown();
document.getElementById('ipam-modal-title').textContent = 'Lisää IPAM-merkintä';
document.getElementById('ipam-modal').style.display = 'flex';
});
document.getElementById('ipam-modal-close')?.addEventListener('click', () => {
document.getElementById('ipam-modal').style.display = 'none';
});
document.getElementById('ipam-form-cancel')?.addEventListener('click', () => {
document.getElementById('ipam-modal').style.display = 'none';
});
document.getElementById('ipam-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('ipam-form-id').value;
const data = {
tyyppi: document.getElementById('ipam-form-tyyppi').value,
verkko: document.getElementById('ipam-form-verkko').value.trim(),
vlan_id: document.getElementById('ipam-form-vlan').value || null,
nimi: document.getElementById('ipam-form-nimi').value.trim(),
site_id: document.getElementById('ipam-form-site').value || null,
tila: document.getElementById('ipam-form-tila').value,
asiakas: document.getElementById('ipam-form-asiakas').value.trim(),
lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
};
if (id) data.id = id;
try {
await apiCall('ipam_save', 'POST', data);
document.getElementById('ipam-modal').style.display = 'none';
loadIpam();
} catch (e) { alert(e.message); }
});
document.getElementById('ipam-search-input')?.addEventListener('input', () => renderIpam());
// ==================== MODUULIT ====================
const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'archive', 'changelog', 'settings'];
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
function applyModules(modules) {
// Yhteensopivuus: vanha 'devices' → 'tekniikka'
if (modules && modules.includes('devices') && !modules.includes('tekniikka')) {
modules = modules.map(m => m === 'devices' ? 'tekniikka' : m);
}
// 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.style.display = logoUrl ? 'none' : ''; if (!logoUrl) loginTitle.textContent = nimi; }
if (loginSubtitle) { loginSubtitle.style.display = logoUrl ? 'none' : ''; if (!logoUrl) 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();