1917 lines
91 KiB
JavaScript
1917 lines
91 KiB
JavaScript
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)
|
|
|
|
// Elements
|
|
const loginScreen = document.getElementById('login-screen');
|
|
const dashboard = document.getElementById('dashboard');
|
|
const loginForm = document.getElementById('login-form');
|
|
const loginError = document.getElementById('login-error');
|
|
const searchInput = document.getElementById('search-input');
|
|
const tbody = document.getElementById('customer-tbody');
|
|
const noCustomers = document.getElementById('no-customers');
|
|
const customerCount = document.getElementById('customer-count');
|
|
const totalBilling = document.getElementById('total-billing');
|
|
const customerModal = document.getElementById('customer-modal');
|
|
const detailModal = document.getElementById('detail-modal');
|
|
const customerForm = document.getElementById('customer-form');
|
|
const userModal = document.getElementById('user-modal');
|
|
|
|
// API helpers
|
|
async function apiCall(action, method = 'GET', body = null) {
|
|
const opts = { method, credentials: 'include' };
|
|
if (body) {
|
|
opts.headers = { 'Content-Type': 'application/json' };
|
|
opts.body = JSON.stringify(body);
|
|
}
|
|
const res = await fetch(`${API}?action=${action}`, opts);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Virhe');
|
|
return data;
|
|
}
|
|
|
|
// ==================== AUTH ====================
|
|
|
|
const forgotBox = document.getElementById('forgot-box');
|
|
const resetBox = document.getElementById('reset-box');
|
|
const loginBox = document.querySelector('.login-box');
|
|
|
|
function showLoginView() {
|
|
loginBox.style.display = '';
|
|
forgotBox.style.display = 'none';
|
|
resetBox.style.display = 'none';
|
|
}
|
|
|
|
function showForgotView() {
|
|
loginBox.style.display = 'none';
|
|
forgotBox.style.display = '';
|
|
resetBox.style.display = 'none';
|
|
}
|
|
|
|
function showResetView() {
|
|
loginBox.style.display = 'none';
|
|
forgotBox.style.display = 'none';
|
|
resetBox.style.display = '';
|
|
}
|
|
|
|
document.getElementById('forgot-link').addEventListener('click', (e) => { e.preventDefault(); showForgotView(); });
|
|
document.getElementById('forgot-back').addEventListener('click', (e) => { e.preventDefault(); showLoginView(); });
|
|
|
|
async function loadCaptcha() {
|
|
try {
|
|
const data = await apiCall('captcha');
|
|
document.getElementById('captcha-question').textContent = data.question;
|
|
} catch (e) {
|
|
document.getElementById('captcha-question').textContent = 'Virhe';
|
|
}
|
|
}
|
|
|
|
// Salasanan palautuspyyntö
|
|
document.getElementById('forgot-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const username = document.getElementById('forgot-username').value;
|
|
const forgotMsg = document.getElementById('forgot-msg');
|
|
const forgotError = document.getElementById('forgot-error');
|
|
forgotMsg.style.display = 'none';
|
|
forgotError.style.display = 'none';
|
|
try {
|
|
await apiCall('password_reset_request', 'POST', { username });
|
|
forgotMsg.textContent = 'Jos käyttäjätunnukselle on sähköposti, palautuslinkki on lähetetty.';
|
|
forgotMsg.style.display = 'block';
|
|
} catch (err) {
|
|
forgotError.textContent = err.message;
|
|
forgotError.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
// Salasanan vaihto (reset token)
|
|
document.getElementById('reset-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const pw1 = document.getElementById('reset-password').value;
|
|
const pw2 = document.getElementById('reset-password2').value;
|
|
const resetMsg = document.getElementById('reset-msg');
|
|
const resetError = document.getElementById('reset-error');
|
|
resetMsg.style.display = 'none';
|
|
resetError.style.display = 'none';
|
|
if (pw1 !== pw2) { resetError.textContent = 'Salasanat eivät täsmää'; resetError.style.display = 'block'; return; }
|
|
const params = new URLSearchParams(window.location.search);
|
|
const token = params.get('reset');
|
|
try {
|
|
await apiCall('password_reset', 'POST', { token, password: pw1 });
|
|
resetMsg.textContent = 'Salasana vaihdettu! Voit nyt kirjautua.';
|
|
resetMsg.style.display = 'block';
|
|
document.getElementById('reset-form').style.display = 'none';
|
|
setTimeout(() => { window.location.href = window.location.pathname; }, 3000);
|
|
} catch (err) {
|
|
resetError.textContent = err.message;
|
|
resetError.style.display = 'block';
|
|
}
|
|
});
|
|
|
|
async function checkAuth() {
|
|
// Tarkista onko URL:ssa reset-token
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('reset')) {
|
|
try {
|
|
const data = await apiCall('validate_reset_token&token=' + encodeURIComponent(params.get('reset')));
|
|
if (data.valid) { showResetView(); return; }
|
|
} catch (e) {}
|
|
showResetView();
|
|
document.getElementById('reset-error').textContent = 'Palautuslinkki on vanhentunut tai virheellinen';
|
|
document.getElementById('reset-error').style.display = 'block';
|
|
document.getElementById('reset-form').style.display = 'none';
|
|
return;
|
|
}
|
|
try {
|
|
const data = await apiCall('check_auth');
|
|
if (data.authenticated) {
|
|
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
|
availableCompanies = data.companies || [];
|
|
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
|
|
showDashboard();
|
|
}
|
|
} catch (e) { /* not logged in */ }
|
|
}
|
|
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const username = document.getElementById('login-username').value;
|
|
const password = document.getElementById('login-password').value;
|
|
const captcha = document.getElementById('login-captcha').value;
|
|
try {
|
|
const data = await apiCall('login', 'POST', { username, password, captcha: parseInt(captcha) });
|
|
loginError.style.display = 'none';
|
|
currentUser = { username: data.username, nimi: data.nimi, role: data.role };
|
|
availableCompanies = data.companies || [];
|
|
currentCompany = availableCompanies.find(c => c.id === data.company_id) || availableCompanies[0] || null;
|
|
showDashboard();
|
|
} catch (err) {
|
|
loginError.textContent = err.message;
|
|
loginError.style.display = 'block';
|
|
document.getElementById('login-captcha').value = '';
|
|
loadCaptcha();
|
|
}
|
|
});
|
|
|
|
document.getElementById('btn-logout').addEventListener('click', async () => {
|
|
await apiCall('logout');
|
|
dashboard.style.display = 'none';
|
|
loginScreen.style.display = 'flex';
|
|
document.getElementById('login-username').value = '';
|
|
document.getElementById('login-password').value = '';
|
|
document.getElementById('login-captcha').value = '';
|
|
showLoginView();
|
|
loadCaptcha();
|
|
});
|
|
|
|
async function showDashboard() {
|
|
loginScreen.style.display = 'none';
|
|
dashboard.style.display = 'block';
|
|
document.getElementById('user-info').textContent = currentUser.nimi || currentUser.username;
|
|
// Näytä admin-toiminnot vain adminille
|
|
document.getElementById('btn-users').style.display = currentUser.role === 'admin' ? '' : 'none';
|
|
document.getElementById('tab-settings').style.display = currentUser.role === 'admin' ? '' : 'none';
|
|
document.getElementById('btn-companies').style.display = currentUser.role === 'admin' ? '' : 'none';
|
|
// Yritysvalitsin
|
|
populateCompanySelector();
|
|
// Avaa oikea tabi URL-hashin perusteella (tai customers oletuks)
|
|
const hash = window.location.hash.replace('#', '');
|
|
const validTabs = ['customers', 'leads', '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 =>
|
|
`<option value="${c.id}" ${currentCompany && c.id === currentCompany.id ? 'selected' : ''}>${esc(c.nimi)}</option>`
|
|
).join('');
|
|
}
|
|
|
|
async function switchCompany(companyId) {
|
|
try {
|
|
await apiCall('company_switch', 'POST', { company_id: companyId });
|
|
currentCompany = availableCompanies.find(c => c.id === companyId) || null;
|
|
// 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 === 'archive') loadArchive();
|
|
if (target === 'changelog') loadChangelog();
|
|
if (target === 'support') { loadTickets(); showTicketListView(); }
|
|
if (target === 'users') loadUsers();
|
|
if (target === 'settings') loadSettings();
|
|
if (target === 'companies') loadCompaniesTab();
|
|
}
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
switchToTab(tab.dataset.tab);
|
|
});
|
|
});
|
|
|
|
// Logo -> Asiakkaat (alkunäkymä)
|
|
document.getElementById('brand-home').addEventListener('click', () => {
|
|
switchToTab('customers');
|
|
});
|
|
|
|
// Käyttäjät-nappi headerissa
|
|
document.getElementById('btn-users').addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
document.getElementById('tab-content-users').classList.add('active');
|
|
loadUsers();
|
|
});
|
|
|
|
document.getElementById('btn-companies').addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
document.getElementById('tab-content-companies').classList.add('active');
|
|
window.location.hash = 'companies';
|
|
loadCompaniesTab();
|
|
});
|
|
|
|
// ==================== CUSTOMERS ====================
|
|
|
|
async function loadCustomers() {
|
|
customers = await apiCall('customers');
|
|
renderTable();
|
|
}
|
|
|
|
function flattenRows(customerList) {
|
|
const rows = [];
|
|
customerList.forEach(c => {
|
|
const liittymat = c.liittymat || [];
|
|
if (liittymat.length === 0) {
|
|
rows.push({ customer: c, liittyma: { asennusosoite: '', postinumero: '', kaupunki: '', liittymanopeus: '', hinta: 0, sopimuskausi: '', alkupvm: '' }, index: 0 });
|
|
} else {
|
|
liittymat.forEach((l, i) => rows.push({ customer: c, liittyma: l, index: i }));
|
|
}
|
|
});
|
|
return rows;
|
|
}
|
|
|
|
function renderTable() {
|
|
const query = searchInput.value.toLowerCase().trim();
|
|
let filtered = customers;
|
|
if (query) {
|
|
filtered = customers.filter(c => {
|
|
const liittymat = c.liittymat || [];
|
|
const inL = liittymat.some(l =>
|
|
(l.asennusosoite || '').toLowerCase().includes(query) ||
|
|
(l.postinumero || '').toLowerCase().includes(query) ||
|
|
(l.kaupunki || '').toLowerCase().includes(query) ||
|
|
(l.liittymanopeus || '').toLowerCase().includes(query)
|
|
);
|
|
return c.yritys.toLowerCase().includes(query) ||
|
|
(c.yhteyshenkilö || '').toLowerCase().includes(query) || inL;
|
|
});
|
|
}
|
|
|
|
const rows = flattenRows(filtered);
|
|
rows.sort((a, b) => {
|
|
let va, vb;
|
|
if (['asennusosoite', 'postinumero', 'kaupunki', 'liittymanopeus', 'hinta', 'sopimuskausi'].includes(sortField)) {
|
|
va = a.liittyma[sortField] ?? '';
|
|
vb = b.liittyma[sortField] ?? '';
|
|
} else {
|
|
va = a.customer[sortField] ?? '';
|
|
vb = b.customer[sortField] ?? '';
|
|
}
|
|
if (sortField === 'hinta') { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; }
|
|
else { va = String(va).toLowerCase(); vb = String(vb).toLowerCase(); }
|
|
if (va < vb) return sortAsc ? -1 : 1;
|
|
if (va > vb) return sortAsc ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
if (rows.length === 0) {
|
|
tbody.innerHTML = '';
|
|
noCustomers.style.display = 'block';
|
|
document.getElementById('customer-table').style.display = 'none';
|
|
} else {
|
|
noCustomers.style.display = 'none';
|
|
document.getElementById('customer-table').style.display = 'table';
|
|
let prevId = null;
|
|
tbody.innerHTML = rows.map(r => {
|
|
const c = r.customer, l = r.liittyma;
|
|
const isFirst = c.id !== prevId;
|
|
prevId = c.id;
|
|
const sopimus = l.sopimuskausi ? l.sopimuskausi + ' kk' : '';
|
|
const alkupvm = l.alkupvm ? ' (' + esc(l.alkupvm) + ')' : '';
|
|
return `<tr data-id="${c.id}" class="${isFirst ? '' : 'sub-row'}">
|
|
<td>${isFirst ? '<strong>' + esc(c.yritys) + '</strong>' : '<span class="sub-marker">↳</span>'}</td>
|
|
<td>${esc(l.asennusosoite)}${l.postinumero ? ', ' + esc(l.postinumero) : ''}</td>
|
|
<td>${esc(l.kaupunki)}</td>
|
|
<td>${esc(l.liittymanopeus)}</td>
|
|
<td class="price-cell">${formatPrice(l.hinta)}</td>
|
|
<td>${sopimus}${alkupvm}</td>
|
|
<td class="actions-cell">${isFirst ? `<button onclick="event.stopPropagation();editCustomer('${c.id}')" title="Muokkaa">✎</button><button onclick="event.stopPropagation();deleteCustomer('${c.id}','${esc(c.yritys)}')" title="Arkistoi">🗃</button>` : ''}</td>
|
|
</tr>`;
|
|
}).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 = '<span style="color:#aaa;font-size:0.85rem;">-</span>';
|
|
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 ? '<span style="color:#aaa;font-size:0.85rem;">-</span>' :
|
|
sorted.map(([sp, cnt]) => {
|
|
const isTop = cnt === maxC;
|
|
const w = Math.max(15, (cnt / maxC) * 50);
|
|
return `<span class="speed-item ${isTop ? 'top' : ''}">${esc(sp)} (${cnt})<span class="speed-bar" style="width:${w}px"></span></span>`;
|
|
}).join('');
|
|
}
|
|
// Keskihinta
|
|
const total = liittymat.reduce((sum, l) => sum + (parseFloat(l.hinta) || 0), 0);
|
|
setText('stat-avg-price', formatPrice(total / connCount));
|
|
}
|
|
|
|
function setTrivia(id, value, sub) {
|
|
const el = document.getElementById(id);
|
|
const subEl = document.getElementById(id + '-detail');
|
|
if (el) el.textContent = value;
|
|
if (subEl) subEl.textContent = sub;
|
|
}
|
|
function setText(id, value) { const el = document.getElementById(id); if (el) el.textContent = value; }
|
|
function formatPrice(val) { return parseFloat(val || 0).toFixed(2).replace('.', ',') + ' €'; }
|
|
function esc(str) { if (!str) return ''; const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
|
|
|
|
// Search & Sort
|
|
searchInput.addEventListener('input', () => renderTable());
|
|
document.querySelectorAll('th[data-sort]').forEach(th => {
|
|
th.addEventListener('click', () => {
|
|
const f = th.dataset.sort;
|
|
if (sortField === f) sortAsc = !sortAsc;
|
|
else { sortField = f; sortAsc = true; }
|
|
renderTable();
|
|
});
|
|
});
|
|
|
|
// Row click
|
|
tbody.addEventListener('click', (e) => {
|
|
const row = e.target.closest('tr');
|
|
if (row) showDetail(row.dataset.id);
|
|
});
|
|
|
|
function detailVal(val) { return val ? esc(val) : '<span class="empty">-</span>'; }
|
|
function detailLink(val, type) {
|
|
if (!val) return '<span class="empty">-</span>';
|
|
if (type === 'tel') return `<a href="tel:${esc(val)}">${esc(val)}</a>`;
|
|
if (type === 'email') return `<a href="mailto:${esc(val)}">${esc(val)}</a>`;
|
|
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 `<div class="liittyma-card">
|
|
${liittymat.length > 1 ? `<div class="liittyma-num">Liittymä ${i + 1}</div>` : ''}
|
|
<div class="detail-grid">
|
|
<div class="detail-item"><div class="detail-label">Osoite</div><div class="detail-value">${detailVal(addr)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Nopeus</div><div class="detail-value">${detailVal(l.liittymanopeus)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Hinta / kk</div><div class="detail-value price-cell">${formatPrice(l.hinta)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Sopimuskausi</div><div class="detail-value">${l.sopimuskausi ? l.sopimuskausi + ' kk' : '-'}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Alkaen</div><div class="detail-value">${detailVal(l.alkupvm)}</div></div>
|
|
</div>
|
|
</div>`;
|
|
}).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 = `
|
|
<div class="detail-section"><h3>Perustiedot</h3><div class="detail-grid">
|
|
<div class="detail-item"><div class="detail-label">Yritys</div><div class="detail-value">${detailVal(c.yritys)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Y-tunnus</div><div class="detail-value">${detailVal(c.ytunnus)}</div></div>
|
|
</div></div>
|
|
<div class="detail-section"><h3>Liittymät (${liittymat.length})</h3>${liittymatHtml}
|
|
${liittymat.length > 1 ? `<div class="liittyma-total">Yhteensä: ${formatPrice(totalH)}/kk</div>` : ''}
|
|
</div>
|
|
<div class="detail-section"><h3>Yhteystiedot</h3><div class="detail-grid">
|
|
<div class="detail-item"><div class="detail-label">Yhteyshenkilö</div><div class="detail-value">${detailVal(c.yhteyshenkilö)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Puhelin</div><div class="detail-value">${detailLink(c.puhelin, 'tel')}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Sähköposti</div><div class="detail-value">${detailLink(c.sahkoposti, 'email')}</div></div>
|
|
</div></div>
|
|
<div class="detail-section"><h3>Laskutustiedot</h3><div class="detail-grid">
|
|
<div class="detail-item"><div class="detail-label">Laskutusosoite</div><div class="detail-value">${detailVal(fullBilling)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Laskutussähköposti</div><div class="detail-value">${detailLink(c.laskutussahkoposti, 'email')}</div></div>
|
|
<div class="detail-item"><div class="detail-label">E-laskuosoite</div><div class="detail-value">${detailVal(c.elaskuosoite)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">E-laskuvälittäjä</div><div class="detail-value">${detailVal(c.elaskuvalittaja)}</div></div>
|
|
</div></div>
|
|
${c.lisatiedot ? `<div class="detail-section"><h3>Lisätiedot</h3><p style="white-space:pre-wrap;color:#555;">${esc(c.lisatiedot)}</p></div>` : ''}
|
|
<div class="detail-section"><h3>Tiedostot</h3>
|
|
<div class="file-upload-area">
|
|
<label class="file-upload-btn btn-primary" style="display:inline-block;cursor:pointer;font-size:0.85rem;padding:8px 16px;">
|
|
+ Lisää tiedosto <input type="file" id="file-upload-input" style="display:none" multiple>
|
|
</label>
|
|
<span style="font-size:0.8rem;color:#999;margin-left:8px;">Max 20 MB / tiedosto</span>
|
|
</div>
|
|
<div id="file-list" class="file-list" style="margin-top:0.75rem;"></div>
|
|
</div>`;
|
|
|
|
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 = '<p style="color:#aaa;font-size:0.85rem;">Ei tiedostoja.</p>'; return; }
|
|
fileList.innerHTML = files.map(f => `<div class="file-item">
|
|
<div class="file-info">
|
|
<a href="${API}?action=file_download&customer_id=${customerId}&filename=${encodeURIComponent(f.filename)}" class="file-name" target="_blank">${esc(f.filename)}</a>
|
|
<span class="file-meta">${formatFileSize(f.size)} · ${f.modified}</span>
|
|
</div>
|
|
<button class="file-delete-btn" onclick="deleteFile('${customerId}','${esc(f.filename)}')" title="Poista">✕</button>
|
|
</div>`).join('');
|
|
} catch (e) { fileList.innerHTML = '<p style="color:#e74c3c;font-size:0.85rem;">Virhe ladattaessa tiedostoja.</p>'; }
|
|
}
|
|
|
|
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 class="liittyma-row-header">
|
|
<span class="liittyma-row-title">Liittymä ${index + 1}</span>
|
|
<button type="button" class="btn-remove-row" title="Poista liittymä">✕</button>
|
|
</div>
|
|
<div class="form-grid form-grid-liittyma">
|
|
<div class="form-group"><label>Osoite</label><input type="text" class="l-asennusosoite" value="${esc(data.asennusosoite || '')}" placeholder="esim. Kauppakatu 5"></div>
|
|
<div class="form-group"><label>Postinumero</label><input type="text" class="l-postinumero" value="${esc(data.postinumero || '')}" placeholder="20100"></div>
|
|
<div class="form-group"><label>Kaupunki</label><input type="text" class="l-kaupunki" value="${esc(data.kaupunki || '')}" placeholder="Turku"></div>
|
|
<div class="form-group"><label>Nopeus</label><input type="text" class="l-liittymanopeus" value="${esc(data.liittymanopeus || '')}" placeholder="esim. 100/100"></div>
|
|
<div class="form-group"><label>Hinta €/kk</label><input type="number" class="l-hinta" step="0.01" min="0" value="${data.hinta || ''}"></div>
|
|
<div class="form-group"><label>Sopimuskausi</label><select class="l-sopimuskausi">
|
|
<option value="">- Valitse -</option>
|
|
<option value="1" ${data.sopimuskausi === '1' ? 'selected' : ''}>1 kk</option>
|
|
<option value="12" ${data.sopimuskausi === '12' ? 'selected' : ''}>12 kk</option>
|
|
<option value="24" ${data.sopimuskausi === '24' ? 'selected' : ''}>24 kk</option>
|
|
<option value="36" ${data.sopimuskausi === '36' ? 'selected' : ''}>36 kk</option>
|
|
</select></div>
|
|
<div class="form-group"><label>Alkaen</label><input type="date" class="l-alkupvm" value="${esc(data.alkupvm || '')}"></div>
|
|
</div>`;
|
|
div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); });
|
|
return div;
|
|
}
|
|
|
|
function renumberLiittymaRows() {
|
|
document.getElementById('liittymat-container').querySelectorAll('.liittyma-row').forEach((row, i) => {
|
|
row.dataset.index = i;
|
|
row.querySelector('.liittyma-row-title').textContent = `Liittymä ${i + 1}`;
|
|
});
|
|
}
|
|
|
|
function collectLiittymatFromForm() {
|
|
return Array.from(document.getElementById('liittymat-container').querySelectorAll('.liittyma-row')).map(row => ({
|
|
asennusosoite: row.querySelector('.l-asennusosoite').value,
|
|
postinumero: row.querySelector('.l-postinumero').value,
|
|
kaupunki: row.querySelector('.l-kaupunki').value,
|
|
liittymanopeus: row.querySelector('.l-liittymanopeus').value,
|
|
hinta: row.querySelector('.l-hinta').value,
|
|
sopimuskausi: row.querySelector('.l-sopimuskausi').value,
|
|
alkupvm: row.querySelector('.l-alkupvm').value,
|
|
}));
|
|
}
|
|
|
|
document.getElementById('btn-add-liittyma').addEventListener('click', () => {
|
|
const container = document.getElementById('liittymat-container');
|
|
container.appendChild(createLiittymaRow({}, container.querySelectorAll('.liittyma-row').length));
|
|
});
|
|
|
|
document.getElementById('form-billing-same').addEventListener('change', function () {
|
|
const bf = document.getElementById('billing-fields');
|
|
if (this.checked) {
|
|
bf.style.display = 'none';
|
|
const first = document.querySelector('.liittyma-row');
|
|
if (first) {
|
|
document.getElementById('form-laskutusosoite').value = first.querySelector('.l-asennusosoite').value;
|
|
document.getElementById('form-laskutuspostinumero').value = first.querySelector('.l-postinumero').value;
|
|
document.getElementById('form-laskutuskaupunki').value = first.querySelector('.l-kaupunki').value;
|
|
}
|
|
} else { bf.style.display = 'block'; }
|
|
});
|
|
|
|
// Add/Edit modal
|
|
document.getElementById('btn-add').addEventListener('click', () => openCustomerForm());
|
|
document.getElementById('modal-close').addEventListener('click', () => customerModal.style.display = 'none');
|
|
document.getElementById('form-cancel').addEventListener('click', () => customerModal.style.display = 'none');
|
|
|
|
function openCustomerForm(customer = null) {
|
|
const c = customer;
|
|
document.getElementById('modal-title').textContent = c ? 'Muokkaa asiakasta' : 'Lisää asiakas';
|
|
document.getElementById('form-submit').textContent = c ? 'Päivitä' : 'Tallenna';
|
|
document.getElementById('form-id').value = c ? c.id : '';
|
|
document.getElementById('form-yritys').value = c ? c.yritys : '';
|
|
document.getElementById('form-ytunnus').value = c ? (c.ytunnus || '') : '';
|
|
document.getElementById('form-yhteyshenkilo').value = c ? (c.yhteyshenkilö || '') : '';
|
|
document.getElementById('form-puhelin').value = c ? (c.puhelin || '') : '';
|
|
document.getElementById('form-sahkoposti').value = c ? (c.sahkoposti || '') : '';
|
|
document.getElementById('form-laskutusosoite').value = c ? (c.laskutusosoite || '') : '';
|
|
document.getElementById('form-laskutuspostinumero').value = c ? (c.laskutuspostinumero || '') : '';
|
|
document.getElementById('form-laskutuskaupunki').value = c ? (c.laskutuskaupunki || '') : '';
|
|
document.getElementById('form-laskutussahkoposti').value = c ? (c.laskutussahkoposti || '') : '';
|
|
document.getElementById('form-elaskuosoite').value = c ? (c.elaskuosoite || '') : '';
|
|
document.getElementById('form-elaskuvalittaja').value = c ? (c.elaskuvalittaja || '') : '';
|
|
document.getElementById('form-lisatiedot').value = c ? (c.lisatiedot || '') : '';
|
|
document.getElementById('form-billing-same').checked = false;
|
|
document.getElementById('billing-fields').style.display = 'block';
|
|
const container = document.getElementById('liittymat-container');
|
|
container.innerHTML = '';
|
|
(c ? (c.liittymat || []) : [{}]).forEach((l, i) => container.appendChild(createLiittymaRow(l, i)));
|
|
customerModal.style.display = 'flex';
|
|
document.getElementById('form-yritys').focus();
|
|
}
|
|
|
|
function editCustomer(id) { const c = customers.find(x => x.id === id); if (c) openCustomerForm(c); }
|
|
|
|
async function deleteCustomer(id, name) {
|
|
if (!confirm(`Arkistoidaanko asiakas "${name}"?\n\nAsiakas siirretään arkistoon, josta sen voi palauttaa.`)) return;
|
|
await apiCall('customer_delete', 'POST', { id });
|
|
await loadCustomers();
|
|
}
|
|
|
|
customerForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const id = document.getElementById('form-id').value;
|
|
if (document.getElementById('form-billing-same').checked) {
|
|
const first = document.querySelector('.liittyma-row');
|
|
if (first) {
|
|
document.getElementById('form-laskutusosoite').value = first.querySelector('.l-asennusosoite').value;
|
|
document.getElementById('form-laskutuspostinumero').value = first.querySelector('.l-postinumero').value;
|
|
document.getElementById('form-laskutuskaupunki').value = first.querySelector('.l-kaupunki').value;
|
|
}
|
|
}
|
|
const data = {
|
|
yritys: document.getElementById('form-yritys').value,
|
|
ytunnus: document.getElementById('form-ytunnus').value,
|
|
yhteyshenkilö: document.getElementById('form-yhteyshenkilo').value,
|
|
puhelin: document.getElementById('form-puhelin').value,
|
|
sahkoposti: document.getElementById('form-sahkoposti').value,
|
|
laskutusosoite: document.getElementById('form-laskutusosoite').value,
|
|
laskutuspostinumero: document.getElementById('form-laskutuspostinumero').value,
|
|
laskutuskaupunki: document.getElementById('form-laskutuskaupunki').value,
|
|
laskutussahkoposti: document.getElementById('form-laskutussahkoposti').value,
|
|
elaskuosoite: document.getElementById('form-elaskuosoite').value,
|
|
elaskuvalittaja: document.getElementById('form-elaskuvalittaja').value,
|
|
lisatiedot: document.getElementById('form-lisatiedot').value,
|
|
liittymat: collectLiittymatFromForm(),
|
|
};
|
|
if (id) { data.id = id; await apiCall('customer_update', 'POST', data); }
|
|
else { await apiCall('customer', 'POST', data); }
|
|
customerModal.style.display = 'none';
|
|
await loadCustomers();
|
|
});
|
|
|
|
// ==================== 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 => `<tr data-lead-id="${l.id}">
|
|
<td><strong>${esc(l.yritys)}</strong></td>
|
|
<td>${esc(l.yhteyshenkilo || '')}</td>
|
|
<td>${esc(l.kaupunki || '')}</td>
|
|
<td><span class="lead-status lead-status-${l.tila || 'uusi'}">${leadStatusLabels[l.tila] || l.tila || 'Uusi'}</span></td>
|
|
<td class="nowrap">${esc((l.luotu || '').substring(0, 10))}</td>
|
|
<td class="actions-cell">
|
|
<button onclick="event.stopPropagation();editLead('${l.id}')" title="Muokkaa">✎</button>
|
|
<button onclick="event.stopPropagation();deleteLead('${l.id}','${esc(l.yritys)}')" title="Poista">🗑</button>
|
|
</td>
|
|
</tr>`).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 = `
|
|
<div style="padding:1.5rem;">
|
|
<div class="detail-grid">
|
|
<div class="detail-item"><div class="detail-label">Yritys</div><div class="detail-value">${detailVal(l.yritys)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Tila</div><div class="detail-value"><span class="lead-status lead-status-${l.tila || 'uusi'}">${leadStatusLabels[l.tila] || 'Uusi'}</span></div></div>
|
|
<div class="detail-item"><div class="detail-label">Yhteyshenkilö</div><div class="detail-value">${detailVal(l.yhteyshenkilo)}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Puhelin</div><div class="detail-value">${detailLink(l.puhelin, 'tel')}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Sähköposti</div><div class="detail-value">${detailLink(l.sahkoposti, 'email')}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Osoite</div><div class="detail-value">${detailVal([l.osoite, l.kaupunki].filter(Boolean).join(', '))}</div></div>
|
|
<div class="detail-item"><div class="detail-label">Lisätty</div><div class="detail-value">${detailVal(l.luotu)} (${esc(l.luoja || '')})</div></div>
|
|
${l.muokattu ? `<div class="detail-item"><div class="detail-label">Muokattu</div><div class="detail-value">${esc(l.muokattu)} (${esc(l.muokkaaja || '')})</div></div>` : ''}
|
|
</div>
|
|
${l.muistiinpanot ? `<div style="margin-top:1.25rem;"><div class="detail-label" style="margin-bottom:0.5rem;">MUISTIINPANOT</div><div style="white-space:pre-wrap;color:#555;background:#f8f9fb;padding:1rem;border-radius:8px;border:1px solid #e8ebf0;font-size:0.9rem;">${esc(l.muistiinpanot)}</div></div>` : ''}
|
|
</div>`;
|
|
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 => `<tr>
|
|
<td><strong>${esc(c.yritys)}</strong></td>
|
|
<td>${(c.liittymat || []).length}</td>
|
|
<td>${esc(c.arkistoitu || '')}</td>
|
|
<td>${esc(c.arkistoija || '')}</td>
|
|
<td class="actions-cell">
|
|
<button onclick="restoreCustomer('${c.id}')" class="btn-small btn-restore" title="Palauta">↺ Palauta</button>
|
|
${currentUser.role === 'admin' ? `<button onclick="permanentDelete('${c.id}','${esc(c.yritys)}')" class="btn-small btn-perm-delete" title="Poista pysyvästi">✕ Poista</button>` : ''}
|
|
</td>
|
|
</tr>`).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 => `<tr>
|
|
<td class="nowrap">${esc(e.timestamp)}</td>
|
|
<td><strong>${esc(e.user)}</strong></td>
|
|
<td>${actionLabels[e.action] || esc(e.action)}</td>
|
|
<td>${esc(e.customer_name)}</td>
|
|
<td class="text-muted">${esc(e.details)}</td>
|
|
</tr>`).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 => `<tr>
|
|
<td><strong>${esc(u.username)}</strong></td>
|
|
<td>${esc(u.nimi)}</td>
|
|
<td>${esc(u.email || '')}</td>
|
|
<td><span class="role-badge role-${u.role}">${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span></td>
|
|
<td>${esc(u.luotu)}</td>
|
|
<td class="actions-cell">
|
|
<button onclick="editUser('${u.id}')" title="Muokkaa">✎</button>
|
|
${u.id !== '${currentUser.id}' ? `<button onclick="deleteUser('${u.id}','${esc(u.username)}')" title="Poista">🗑</button>` : ''}
|
|
</td>
|
|
</tr>`).join('');
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
let usersCache = [];
|
|
document.getElementById('btn-add-user').addEventListener('click', () => openUserForm());
|
|
document.getElementById('user-modal-close').addEventListener('click', () => userModal.style.display = 'none');
|
|
document.getElementById('user-form-cancel').addEventListener('click', () => userModal.style.display = 'none');
|
|
|
|
function openUserForm(user = null) {
|
|
document.getElementById('user-modal-title').textContent = user ? 'Muokkaa käyttäjää' : 'Lisää käyttäjä';
|
|
document.getElementById('user-form-id').value = user ? user.id : '';
|
|
document.getElementById('user-form-username').value = user ? user.username : '';
|
|
document.getElementById('user-form-username').disabled = !!user;
|
|
document.getElementById('user-form-nimi').value = user ? user.nimi : '';
|
|
document.getElementById('user-form-email').value = user ? (user.email || '') : '';
|
|
document.getElementById('user-form-password').value = '';
|
|
document.getElementById('user-pw-hint').textContent = user ? '(jätä tyhjäksi jos ei muuteta)' : '*';
|
|
document.getElementById('user-form-role').value = user ? user.role : 'user';
|
|
// Yrityscheckboxit
|
|
const allComps = availableCompanies.length > 0 ? availableCompanies : [];
|
|
const userComps = user ? (user.companies || []) : [];
|
|
const container = document.getElementById('user-company-checkboxes');
|
|
// Hae kaikki yritykset admin-näkymää varten
|
|
apiCall('companies_all').then(companies => {
|
|
container.innerHTML = companies.map(c =>
|
|
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
|
|
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
|
|
${esc(c.nimi)}
|
|
</label>`
|
|
).join('');
|
|
}).catch(() => {
|
|
container.innerHTML = allComps.map(c =>
|
|
`<label style="display:flex;align-items:center;gap:0.3rem;cursor:pointer;">
|
|
<input type="checkbox" class="user-company-cb" value="${c.id}" ${userComps.includes(c.id) ? 'checked' : ''}>
|
|
${esc(c.nimi)}
|
|
</label>`
|
|
).join('');
|
|
});
|
|
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);
|
|
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,
|
|
};
|
|
const pw = document.getElementById('user-form-password').value;
|
|
if (pw) data.password = pw;
|
|
else if (!id) { alert('Salasana vaaditaan uudelle käyttäjälle'); return; }
|
|
try {
|
|
if (id) { data.id = id; await apiCall('user_update', 'POST', data); }
|
|
else { await apiCall('user_create', 'POST', data); }
|
|
userModal.style.display = 'none';
|
|
loadUsers();
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
// ==================== 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))
|
|
);
|
|
}
|
|
|
|
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.status === 'kasittelyssa' ? 'ticket-row-active' : '';
|
|
const checked = bulkSelectedIds.has(t.id) ? 'checked' : '';
|
|
const companyBadge = multiCompany && t.company_name ? `<span class="company-badge">${esc(t.company_name)}</span> ` : '';
|
|
return `<tr data-ticket-id="${t.id}" data-company-id="${t.company_id || ''}" class="${rowClass}">
|
|
<td onclick="event.stopPropagation()"><input type="checkbox" class="ticket-checkbox" data-ticket-id="${t.id}" ${checked}></td>
|
|
<td><span class="ticket-status ticket-status-${t.status}">${ticketStatusLabels[t.status] || t.status}</span></td>
|
|
<td><span class="ticket-type ticket-type-${t.type || 'muu'}">${typeLabel}</span></td>
|
|
<td>${companyBadge}<strong>${esc(t.subject)}</strong></td>
|
|
<td>${esc(t.mailbox_name || t.from_name || t.from_email)}</td>
|
|
<td>${t.customer_name ? esc(t.customer_name) : '<span style="color:#ccc;">-</span>'}</td>
|
|
<td style="text-align:center;">${lastType} ${t.message_count}</td>
|
|
<td class="nowrap">${esc((t.updated || '').substring(0, 16))}</td>
|
|
</tr>`;
|
|
}).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-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 = `
|
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1rem;margin-bottom:1.25rem;">
|
|
<div>
|
|
<h2 style="color:#0f3460;margin-bottom:0.25rem;font-size:1.2rem;">${esc(ticket.subject)}</h2>
|
|
<div style="font-size:0.85rem;color:#888;">
|
|
${esc(ticket.from_name)} <${esc(ticket.from_email)}> · Luotu ${esc(ticket.created)}
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
|
<select id="ticket-type-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
|
|
<option value="muu" ${(ticket.type || 'muu') === 'muu' ? 'selected' : ''}>Muu</option>
|
|
<option value="laskutus" ${ticket.type === 'laskutus' ? 'selected' : ''}>Laskutus</option>
|
|
<option value="tekniikka" ${ticket.type === 'tekniikka' ? 'selected' : ''}>Tekniikka</option>
|
|
<option value="vika" ${ticket.type === 'vika' ? 'selected' : ''}>Vika</option>
|
|
</select>
|
|
<select id="ticket-status-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
|
|
<option value="uusi" ${ticket.status === 'uusi' ? 'selected' : ''}>Uusi</option>
|
|
<option value="kasittelyssa" ${ticket.status === 'kasittelyssa' ? 'selected' : ''}>Käsittelyssä</option>
|
|
<option value="odottaa" ${ticket.status === 'odottaa' ? 'selected' : ''}>Odottaa vastausta</option>
|
|
<option value="ratkaistu" ${ticket.status === 'ratkaistu' ? 'selected' : ''}>Ratkaistu</option>
|
|
<option value="suljettu" ${ticket.status === 'suljettu' ? 'selected' : ''}>Suljettu</option>
|
|
</select>
|
|
<select id="ticket-assign-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
|
|
<option value="">Ei osoitettu</option>
|
|
</select>
|
|
<select id="ticket-customer-select" style="padding:6px 10px;border:2px solid #e0e0e0;border-radius:8px;font-size:0.85rem;">
|
|
<option value="">Ei asiakkuutta</option>
|
|
</select>
|
|
<button class="btn-danger" id="btn-ticket-delete" style="padding:6px 12px;font-size:0.82rem;">Poista</button>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;margin-top:0.5rem;">
|
|
<span style="font-size:0.82rem;color:#888;font-weight:600;">Tagit:</span>
|
|
<div id="ticket-tags-container" style="display:flex;gap:0.35rem;flex-wrap:wrap;align-items:center;">
|
|
${(ticket.tags || []).map(tag => '<span class="ticket-tag ticket-tag-editable" data-tag="' + esc(tag) + '">#' + esc(tag) + ' <button class="ticket-tag-remove" title="Poista">×</button></span>').join('')}
|
|
</div>
|
|
<div style="display:flex;gap:0.3rem;align-items:center;">
|
|
<input type="text" id="ticket-tag-input" placeholder="+ Lisää tagi" style="padding:4px 8px;border:1px solid #ddd;border-radius:6px;font-size:0.82rem;width:120px;">
|
|
</div>
|
|
${ticket.auto_close_at ? '<span style="font-size:0.78rem;color:#e67e22;margin-left:0.5rem;">⏰ Auto-close: ' + esc(ticket.auto_close_at.substring(0, 10)) + '</span>' : ''}
|
|
</div>`;
|
|
|
|
// Load users for assignment dropdown
|
|
try {
|
|
const users = await apiCall('users');
|
|
const assignSelect = document.getElementById('ticket-assign-select');
|
|
users.forEach(u => {
|
|
const opt = document.createElement('option');
|
|
opt.value = u.username;
|
|
opt.textContent = u.nimi || u.username;
|
|
if (u.username === ticket.assigned_to) opt.selected = true;
|
|
assignSelect.appendChild(opt);
|
|
});
|
|
} catch (e) { /* non-admin may not access users */ }
|
|
|
|
// Type change handler
|
|
document.getElementById('ticket-type-select').addEventListener('change', async function() {
|
|
try {
|
|
await apiCall('ticket_type' + ticketCompanyParam(), 'POST', { id: currentTicketId, type: this.value });
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
// Status change handler
|
|
document.getElementById('ticket-status-select').addEventListener('change', async function() {
|
|
try {
|
|
await apiCall('ticket_status' + ticketCompanyParam(), 'POST', { id: currentTicketId, status: this.value });
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
// Assign handler
|
|
document.getElementById('ticket-assign-select').addEventListener('change', async function() {
|
|
try {
|
|
await apiCall('ticket_assign' + ticketCompanyParam(), 'POST', { id: currentTicketId, assigned_to: this.value });
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
// Customer link — load customers dropdown
|
|
try {
|
|
const custSelect = document.getElementById('ticket-customer-select');
|
|
customers.forEach(c => {
|
|
const opt = document.createElement('option');
|
|
opt.value = c.id;
|
|
opt.textContent = c.yritys;
|
|
if (c.id === ticket.customer_id) opt.selected = true;
|
|
custSelect.appendChild(opt);
|
|
});
|
|
} catch (e) {}
|
|
|
|
document.getElementById('ticket-customer-select').addEventListener('change', async function() {
|
|
const selOpt = this.options[this.selectedIndex];
|
|
const custName = this.value ? selOpt.textContent : '';
|
|
try {
|
|
await apiCall('ticket_customer' + ticketCompanyParam(), 'POST', { id: currentTicketId, customer_id: this.value, customer_name: custName });
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
// 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 `<div class="ticket-message ${typeClass}">
|
|
<div class="ticket-msg-header">
|
|
<span class="ticket-msg-type">${typeIcon}</span>
|
|
<strong>${esc(m.from_name || m.from)}</strong>
|
|
<span class="ticket-msg-time">${esc(m.timestamp)}</span>
|
|
</div>
|
|
<div class="ticket-msg-body">${esc(m.body)}</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
// Show detail, hide list
|
|
document.getElementById('ticket-list-view').style.display = 'none';
|
|
document.getElementById('ticket-detail-view').style.display = 'block';
|
|
|
|
// Reset reply form
|
|
document.getElementById('ticket-reply-body').value = '';
|
|
document.getElementById('ticket-reply-body').placeholder = 'Kirjoita vastaus...';
|
|
ticketReplyType = 'reply';
|
|
document.querySelectorAll('.btn-reply-tab').forEach(b => b.classList.remove('active'));
|
|
document.querySelector('.btn-reply-tab[data-reply-type="reply"]').classList.add('active');
|
|
document.getElementById('btn-send-reply').textContent = 'Lähetä vastaus';
|
|
|
|
} catch (e) { alert(e.message); }
|
|
}
|
|
|
|
function showTicketListView() {
|
|
document.getElementById('ticket-detail-view').style.display = 'none';
|
|
document.getElementById('ticket-rules-view').style.display = 'none';
|
|
document.getElementById('ticket-list-view').style.display = 'block';
|
|
currentTicketId = null;
|
|
// Reset bulk selection
|
|
bulkSelectedIds.clear();
|
|
const selectAll = document.getElementById('bulk-select-all');
|
|
if (selectAll) selectAll.checked = false;
|
|
updateBulkToolbar();
|
|
}
|
|
|
|
document.getElementById('btn-ticket-back').addEventListener('click', () => {
|
|
showTicketListView();
|
|
loadTickets();
|
|
});
|
|
|
|
// Reply type tabs
|
|
document.querySelectorAll('.btn-reply-tab').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.btn-reply-tab').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
ticketReplyType = btn.dataset.replyType;
|
|
const textarea = document.getElementById('ticket-reply-body');
|
|
const sendBtn = document.getElementById('btn-send-reply');
|
|
if (ticketReplyType === 'note') {
|
|
textarea.placeholder = 'Kirjoita sisäinen muistiinpano...';
|
|
sendBtn.textContent = 'Tallenna muistiinpano';
|
|
} else {
|
|
textarea.placeholder = 'Kirjoita vastaus...';
|
|
sendBtn.textContent = 'Lähetä vastaus';
|
|
}
|
|
});
|
|
});
|
|
|
|
// 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';
|
|
await apiCall(action + ticketCompanyParam(), 'POST', { id: currentTicketId, body });
|
|
// 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 = '<div style="text-align:center;padding:2rem;color:#aaa;">Ei sääntöjä vielä. Lisää ensimmäinen sääntö.</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = ticketRules.map(r => {
|
|
const conditions = [];
|
|
if (r.from_contains) conditions.push('Lähettäjä: <strong>' + esc(r.from_contains) + '</strong>');
|
|
if (r.subject_contains) conditions.push('Otsikko: <strong>' + esc(r.subject_contains) + '</strong>');
|
|
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 `<div style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem 1rem;background:${r.enabled ? '#f8f9fb' : '#fafafa'};border:1px solid #e8ebf0;border-radius:8px;margin-bottom:0.5rem;opacity:${r.enabled ? '1' : '0.5'};">
|
|
<div>
|
|
<div style="font-weight:600;color:#0f3460;font-size:0.9rem;">${esc(r.name)}</div>
|
|
<div style="font-size:0.8rem;color:#888;margin-top:2px;">
|
|
${conditions.length ? 'Ehdot: ' + conditions.join(', ') : '<em>Ei ehtoja</em>'} → ${actions.length ? actions.join(', ') : '<em>Ei toimenpiteitä</em>'}
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:0.4rem;align-items:center;flex-shrink:0;">
|
|
<label style="cursor:pointer;font-size:0.8rem;color:#888;display:flex;align-items:center;gap:3px;">
|
|
<input type="checkbox" ${r.enabled ? 'checked' : ''} onchange="toggleRule('${r.id}', this.checked)"> Päällä
|
|
</label>
|
|
<button onclick="editRule('${r.id}')" style="background:none;border:none;cursor:pointer;font-size:1rem;padding:4px;">✎</button>
|
|
<button onclick="deleteRule('${r.id}')" style="background:none;border:none;cursor:pointer;font-size:1rem;padding:4px;color:#e74c3c;">🗑</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function showRulesView() {
|
|
document.getElementById('ticket-list-view').style.display = 'none';
|
|
document.getElementById('ticket-detail-view').style.display = 'none';
|
|
document.getElementById('ticket-rules-view').style.display = 'block';
|
|
loadRules();
|
|
}
|
|
|
|
function hideRulesView() {
|
|
document.getElementById('ticket-rules-view').style.display = 'none';
|
|
document.getElementById('ticket-list-view').style.display = 'block';
|
|
}
|
|
|
|
function showRuleForm(rule) {
|
|
document.getElementById('rule-form-container').style.display = '';
|
|
document.getElementById('rule-form-title').textContent = rule ? 'Muokkaa sääntöä' : 'Uusi sääntö';
|
|
document.getElementById('rule-form-id').value = rule ? rule.id : '';
|
|
document.getElementById('rule-form-name').value = rule ? rule.name : '';
|
|
document.getElementById('rule-form-from').value = rule ? rule.from_contains : '';
|
|
document.getElementById('rule-form-subject').value = rule ? rule.subject_contains : '';
|
|
document.getElementById('rule-form-status').value = rule ? (rule.set_status || '') : '';
|
|
document.getElementById('rule-form-type').value = rule ? (rule.set_type || '') : '';
|
|
document.getElementById('rule-form-tags').value = rule ? (rule.set_tags || '') : '';
|
|
document.getElementById('rule-form-autoclose').value = rule ? (rule.auto_close_days || '') : '';
|
|
editingRuleId = rule ? rule.id : null;
|
|
}
|
|
|
|
function hideRuleForm() {
|
|
document.getElementById('rule-form-container').style.display = 'none';
|
|
editingRuleId = null;
|
|
}
|
|
|
|
document.getElementById('btn-ticket-rules').addEventListener('click', () => showRulesView());
|
|
document.getElementById('btn-rules-back').addEventListener('click', () => hideRulesView());
|
|
document.getElementById('btn-add-rule').addEventListener('click', () => showRuleForm(null));
|
|
document.getElementById('btn-cancel-rule').addEventListener('click', () => hideRuleForm());
|
|
|
|
document.getElementById('btn-save-rule').addEventListener('click', async () => {
|
|
const name = document.getElementById('rule-form-name').value.trim();
|
|
if (!name) { alert('Nimi puuttuu'); return; }
|
|
const data = {
|
|
name,
|
|
from_contains: document.getElementById('rule-form-from').value.trim(),
|
|
subject_contains: document.getElementById('rule-form-subject').value.trim(),
|
|
set_status: document.getElementById('rule-form-status').value,
|
|
set_type: document.getElementById('rule-form-type').value,
|
|
set_tags: document.getElementById('rule-form-tags').value.trim(),
|
|
auto_close_days: parseInt(document.getElementById('rule-form-autoclose').value) || 0,
|
|
enabled: true,
|
|
};
|
|
const existingId = document.getElementById('rule-form-id').value;
|
|
if (existingId) data.id = existingId;
|
|
try {
|
|
await apiCall('ticket_rule_save', 'POST', data);
|
|
hideRuleForm();
|
|
await loadRules();
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
async function editRule(id) {
|
|
const rule = ticketRules.find(r => r.id === id);
|
|
if (rule) showRuleForm(rule);
|
|
}
|
|
|
|
async function deleteRule(id) {
|
|
if (!confirm('Poistetaanko sääntö?')) return;
|
|
try {
|
|
await apiCall('ticket_rule_delete', 'POST', { id });
|
|
await loadRules();
|
|
} catch (e) { alert(e.message); }
|
|
}
|
|
|
|
async function toggleRule(id, enabled) {
|
|
const rule = ticketRules.find(r => r.id === id);
|
|
if (!rule) return;
|
|
try {
|
|
await apiCall('ticket_rule_save', 'POST', { ...rule, enabled });
|
|
await loadRules();
|
|
} catch (e) { alert(e.message); }
|
|
}
|
|
|
|
// ==================== BULK ACTIONS ====================
|
|
|
|
let bulkSelectedIds = new Set();
|
|
|
|
function updateBulkToolbar() {
|
|
const toolbar = document.getElementById('bulk-actions-toolbar');
|
|
if (bulkSelectedIds.size > 0) {
|
|
toolbar.style.display = 'flex';
|
|
document.getElementById('bulk-count').textContent = bulkSelectedIds.size + ' valittu';
|
|
} else {
|
|
toolbar.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function bulkCloseSelected() {
|
|
if (bulkSelectedIds.size === 0) return;
|
|
if (!confirm(`Suljetaanko ${bulkSelectedIds.size} tikettiä?`)) return;
|
|
try {
|
|
await apiCall('ticket_bulk_status', 'POST', { ids: [...bulkSelectedIds], status: 'suljettu' });
|
|
bulkSelectedIds.clear();
|
|
updateBulkToolbar();
|
|
await loadTickets();
|
|
} catch (e) { alert(e.message); }
|
|
}
|
|
|
|
async function bulkDeleteSelected() {
|
|
if (bulkSelectedIds.size === 0) return;
|
|
if (!confirm(`Poistetaanko ${bulkSelectedIds.size} tikettiä pysyvästi?`)) return;
|
|
try {
|
|
await apiCall('ticket_bulk_delete', 'POST', { ids: [...bulkSelectedIds] });
|
|
bulkSelectedIds.clear();
|
|
updateBulkToolbar();
|
|
await loadTickets();
|
|
} catch (e) { alert(e.message); }
|
|
}
|
|
|
|
// ==================== SETTINGS ====================
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const config = await apiCall('config');
|
|
document.getElementById('settings-api-key').value = config.api_key || '';
|
|
document.getElementById('settings-cors').value = (config.cors_origins || ['https://cuitunet.fi', 'https://www.cuitunet.fi']).join('\n');
|
|
const key = config.api_key || 'AVAIN';
|
|
document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`;
|
|
} catch (e) { console.error(e); }
|
|
}
|
|
|
|
document.getElementById('btn-generate-key').addEventListener('click', async () => {
|
|
try {
|
|
const config = await apiCall('generate_api_key', 'POST');
|
|
document.getElementById('settings-api-key').value = config.api_key || '';
|
|
document.getElementById('api-example-url').textContent = `api.php?action=saatavuus&key=${config.api_key}&osoite=Kauppakatu+5&postinumero=20100&kaupunki=Turku`;
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
document.getElementById('btn-save-settings').addEventListener('click', async () => {
|
|
try {
|
|
const config = await apiCall('config_update', 'POST', {
|
|
api_key: document.getElementById('settings-api-key').value,
|
|
cors_origins: document.getElementById('settings-cors').value,
|
|
});
|
|
alert('Asetukset tallennettu!');
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
|
|
document.getElementById('btn-test-api').addEventListener('click', async () => {
|
|
const osoite = document.getElementById('test-api-address').value.trim();
|
|
const postinumero = document.getElementById('test-api-zip').value.trim();
|
|
const kaupunki = document.getElementById('test-api-city').value.trim();
|
|
const apiKey = document.getElementById('settings-api-key').value;
|
|
if (!osoite || !postinumero || !kaupunki) { alert('Täytä osoite, postinumero ja kaupunki'); return; }
|
|
const result = document.getElementById('test-api-result');
|
|
result.style.display = 'block';
|
|
result.textContent = 'Haetaan...';
|
|
try {
|
|
const params = `osoite=${encodeURIComponent(osoite)}&postinumero=${encodeURIComponent(postinumero)}&kaupunki=${encodeURIComponent(kaupunki)}`;
|
|
const res = await fetch(`${API}?action=saatavuus&key=${encodeURIComponent(apiKey)}&${params}`);
|
|
const data = await res.json();
|
|
result.textContent = JSON.stringify(data, null, 2);
|
|
} catch (e) { result.textContent = 'Virhe: ' + e.message; }
|
|
});
|
|
|
|
// ==================== MODALS ====================
|
|
|
|
customerModal.addEventListener('click', (e) => { if (e.target === customerModal) customerModal.style.display = 'none'; });
|
|
detailModal.addEventListener('click', (e) => { if (e.target === detailModal) detailModal.style.display = 'none'; });
|
|
userModal.addEventListener('click', (e) => { if (e.target === userModal) userModal.style.display = 'none'; });
|
|
leadModal.addEventListener('click', (e) => { if (e.target === leadModal) leadModal.style.display = 'none'; });
|
|
leadDetailModal.addEventListener('click', (e) => { if (e.target === leadDetailModal) leadDetailModal.style.display = 'none'; });
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
customerModal.style.display = 'none';
|
|
detailModal.style.display = 'none';
|
|
userModal.style.display = 'none';
|
|
leadModal.style.display = 'none';
|
|
leadDetailModal.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// ==================== COMPANY SELECTOR ====================
|
|
|
|
document.getElementById('company-selector').addEventListener('change', function () {
|
|
switchCompany(this.value);
|
|
});
|
|
|
|
// ==================== YRITYKSET-TAB (admin) ====================
|
|
|
|
let companiesTabData = [];
|
|
let currentCompanyDetail = null;
|
|
|
|
async function loadCompaniesTab() {
|
|
try {
|
|
companiesTabData = await apiCall('companies_all');
|
|
renderCompaniesTable();
|
|
} catch (e) {
|
|
console.error(e);
|
|
// Fallback: käytä availableCompanies
|
|
companiesTabData = availableCompanies;
|
|
renderCompaniesTable();
|
|
}
|
|
}
|
|
|
|
function renderCompaniesTable() {
|
|
const tbody = document.getElementById('companies-tbody');
|
|
tbody.innerHTML = companiesTabData.map(c => `<tr>
|
|
<td><code>${esc(c.id)}</code></td>
|
|
<td><strong>${esc(c.nimi)}</strong></td>
|
|
<td>-</td>
|
|
<td class="nowrap">${esc((c.luotu || '').substring(0, 10))}</td>
|
|
<td>${c.aktiivinen !== false ? '<span style="color:#22c55e;">Aktiivinen</span>' : '<span style="color:#888;">Ei aktiivinen</span>'}</td>
|
|
<td>
|
|
<button class="btn-link" onclick="showCompanyDetail('${c.id}')">Asetukset</button>
|
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteCompany('${c.id}','${esc(c.nimi)}')">Poista</button>
|
|
</td>
|
|
</tr>`).join('');
|
|
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 : '';
|
|
|
|
// Vaihda aktiivinen yritys jotta API-kutsut kohdistuvat oikein
|
|
await apiCall('company_switch', 'POST', { company_id: id });
|
|
|
|
// Lataa postilaatikot
|
|
loadMailboxes();
|
|
// 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();
|
|
});
|
|
|
|
document.getElementById('btn-save-company-name').addEventListener('click', async () => {
|
|
const nimi = document.getElementById('company-edit-nimi').value.trim();
|
|
if (!nimi) return;
|
|
try {
|
|
await apiCall('company_update', 'POST', { id: currentCompanyDetail, nimi });
|
|
alert('Nimi tallennettu!');
|
|
// Päivitä paikalliset tiedot
|
|
const comp = companiesTabData.find(c => c.id === currentCompanyDetail);
|
|
if (comp) comp.nimi = nimi;
|
|
const avail = availableCompanies.find(c => c.id === currentCompanyDetail);
|
|
if (avail) avail.nimi = nimi;
|
|
populateCompanySelector();
|
|
} 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 = '<p style="color:#888;font-size:0.9rem;">Ei postilaatikoita. Lisää ensimmäinen postilaatikko.</p>';
|
|
return;
|
|
}
|
|
container.innerHTML = mailboxesData.map(mb => `<div class="mailbox-item" style="display:flex;justify-content:space-between;align-items:center;padding:0.75rem;background:#fff;border:1px solid #e0e0e0;border-radius:8px;margin-bottom:0.5rem;">
|
|
<div>
|
|
<strong>${esc(mb.nimi)}</strong>
|
|
<span style="color:#888;font-size:0.85rem;margin-left:0.75rem;">${esc(mb.imap_user)}</span>
|
|
<span style="color:${mb.aktiivinen !== false ? '#22c55e' : '#888'};font-size:0.8rem;margin-left:0.5rem;">${mb.aktiivinen !== false ? 'Aktiivinen' : 'Ei aktiivinen'}</span>
|
|
</div>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
<button class="btn-link" onclick="editMailbox('${mb.id}')">Muokkaa</button>
|
|
<button class="btn-link" style="color:#dc2626;" onclick="deleteMailbox('${mb.id}','${esc(mb.nimi)}')">Poista</button>
|
|
</div>
|
|
</div>`).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 `<label style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0;cursor:pointer;">
|
|
<input type="checkbox" class="company-user-cb" data-user-id="${u.id}" ${hasAccess ? 'checked' : ''} onchange="toggleCompanyUser('${u.id}','${companyId}',this.checked)">
|
|
<strong>${esc(u.nimi || u.username)}</strong>
|
|
<span style="color:#888;font-size:0.85rem;">(${u.username}) — ${u.role === 'admin' ? 'Ylläpitäjä' : 'Käyttäjä'}</span>
|
|
</label>`;
|
|
}).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); }
|
|
}
|
|
|
|
// Init
|
|
loadCaptcha();
|
|
checkAuth();
|