feat: Laitteet-moduuli (inventaario) + sijaintien hallinta + login-fix
- Uusi "Laitteet" välilehti navigaatiossa (devices-moduuli) - Taulukko: Nimi, Hallintaosoite, Serial, Sijainti, Funktio, Tyyppi, Malli, Ping - Lisää/muokkaa/poista laitteita modaali-lomakkeella - Hakupalkki suodattaa kaikista kentistä - Ping-check täppä valmiina tulevaa toteutusta varten - Sijainnit (Sites) -hallinta yrityksen asetuksissa - Lisää/muokkaa/poista sijainteja (toimipisteet, konesalit) - Sijainnit näkyvät laitelomakkeen dropdownissa - Laitteet-moduuli lisätty moduulijärjestelmään (checkbox yritysasetuksissa) - DB: sites + devices taulut, CRUD-funktiot - Fix: Login-näkymä ei enää vilku refreshissä (piilotettu oletuksena) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
217
script.js
217
script.js
@@ -119,6 +119,7 @@ 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; }
|
||||
@@ -139,8 +140,11 @@ async function checkAuth() {
|
||||
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) => {
|
||||
@@ -190,7 +194,7 @@ async function showDashboard() {
|
||||
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 validTabs = ['customers', 'leads', 'devices', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
|
||||
const startTab = validTabs.includes(hash) ? hash : 'customers';
|
||||
switchToTab(startTab);
|
||||
}
|
||||
@@ -237,6 +241,7 @@ function switchToTab(target) {
|
||||
// Lataa sisältö tarvittaessa
|
||||
if (target === 'customers') loadCustomers();
|
||||
if (target === 'leads') loadLeads();
|
||||
if (target === 'devices') loadDevices();
|
||||
if (target === 'archive') loadArchive();
|
||||
if (target === 'changelog') loadChangelog();
|
||||
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
|
||||
@@ -2075,6 +2080,8 @@ async function showCompanyDetail(id) {
|
||||
|
||||
// Lataa postilaatikot
|
||||
loadMailboxes();
|
||||
// Lataa sijainnit
|
||||
loadSites();
|
||||
// Lataa käyttäjäoikeudet
|
||||
loadCompanyUsers(id);
|
||||
}
|
||||
@@ -2337,9 +2344,215 @@ async function toggleCompanyUser(userId, companyId, add) {
|
||||
} 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' ? '🔴' : '⚪')) : '<span style="color:#ccc;">—</span>';
|
||||
return `<tr>
|
||||
<td><strong>${esc(d.nimi)}</strong></td>
|
||||
<td><code style="font-size:0.82rem;">${esc(d.hallintaosoite || '-')}</code></td>
|
||||
<td style="font-size:0.85rem;">${esc(d.serial || '-')}</td>
|
||||
<td>${d.site_name ? esc(d.site_name) : '<span style="color:#ccc;">-</span>'}</td>
|
||||
<td>${esc(d.funktio || '-')}</td>
|
||||
<td>${esc(d.tyyppi || '-')}</td>
|
||||
<td>${esc(d.malli || '-')}</td>
|
||||
<td style="text-align:center;">${pingIcon}</td>
|
||||
<td class="actions-cell">
|
||||
<button class="btn-link" onclick="editDevice('${d.id}')">✎</button>
|
||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteDevice('${d.id}','${esc(d.nimi)}')">🗑</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).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 = '<option value="">— Ei sijaintia —</option>' +
|
||||
sitesData.map(s => `<option value="${s.id}">${esc(s.nimi)}${s.kaupunki ? ' (' + esc(s.kaupunki) + ')' : ''}</option>`).join('');
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
document.getElementById('btn-add-device')?.addEventListener('click', async () => {
|
||||
document.getElementById('device-form-id').value = '';
|
||||
document.getElementById('device-form').reset();
|
||||
await loadSitesForDropdown();
|
||||
document.getElementById('device-modal-title').textContent = 'Lisää laite';
|
||||
document.getElementById('device-modal').style.display = 'flex';
|
||||
});
|
||||
|
||||
document.getElementById('device-modal-close')?.addEventListener('click', () => {
|
||||
document.getElementById('device-modal').style.display = 'none';
|
||||
});
|
||||
document.getElementById('device-form-cancel')?.addEventListener('click', () => {
|
||||
document.getElementById('device-modal').style.display = 'none';
|
||||
});
|
||||
|
||||
document.getElementById('device-form')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('device-form-id').value;
|
||||
const data = {
|
||||
nimi: document.getElementById('device-form-nimi').value.trim(),
|
||||
hallintaosoite: document.getElementById('device-form-hallintaosoite').value.trim(),
|
||||
serial: document.getElementById('device-form-serial').value.trim(),
|
||||
site_id: document.getElementById('device-form-site').value || null,
|
||||
funktio: document.getElementById('device-form-funktio').value.trim(),
|
||||
tyyppi: document.getElementById('device-form-tyyppi').value.trim(),
|
||||
malli: document.getElementById('device-form-malli').value.trim(),
|
||||
ping_check: document.getElementById('device-form-ping-check').checked,
|
||||
lisatiedot: document.getElementById('device-form-lisatiedot').value.trim(),
|
||||
};
|
||||
if (id) data.id = id;
|
||||
try {
|
||||
await apiCall('device_save', 'POST', data);
|
||||
document.getElementById('device-modal').style.display = 'none';
|
||||
loadDevices();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
document.getElementById('device-search-input')?.addEventListener('input', () => renderDevices());
|
||||
|
||||
// ==================== SIJAINNIT (SITES) HALLINTA ====================
|
||||
|
||||
async function loadSites() {
|
||||
try {
|
||||
sitesData = await apiCall('sites');
|
||||
renderSites();
|
||||
} catch (e) { console.error(e); }
|
||||
}
|
||||
|
||||
function renderSites() {
|
||||
const container = document.getElementById('sites-list');
|
||||
if (!container) return;
|
||||
if (sitesData.length === 0) {
|
||||
container.innerHTML = '<p style="color:#888;font-size:0.9rem;">Ei sijainteja. Lisää ensimmäinen sijainti.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = sitesData.map(s => `<div 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(s.nimi)}</strong>
|
||||
${s.osoite ? `<span style="color:#888;font-size:0.85rem;margin-left:0.75rem;">${esc(s.osoite)}</span>` : ''}
|
||||
${s.kaupunki ? `<span style="color:#888;font-size:0.85rem;margin-left:0.5rem;">${esc(s.kaupunki)}</span>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button class="btn-link" onclick="editSite('${s.id}')">Muokkaa</button>
|
||||
<button class="btn-link" style="color:#dc2626;" onclick="deleteSite('${s.id}','${esc(s.nimi)}')">Poista</button>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function editSite(id) {
|
||||
const s = sitesData.find(x => x.id === id);
|
||||
if (!s) return;
|
||||
document.getElementById('site-form-id').value = s.id;
|
||||
document.getElementById('site-form-nimi').value = s.nimi || '';
|
||||
document.getElementById('site-form-osoite').value = s.osoite || '';
|
||||
document.getElementById('site-form-kaupunki').value = s.kaupunki || '';
|
||||
document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia';
|
||||
document.getElementById('site-form-container').style.display = '';
|
||||
}
|
||||
|
||||
async function deleteSite(id, name) {
|
||||
if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return;
|
||||
try {
|
||||
await apiCall('site_delete', 'POST', { id });
|
||||
loadSites();
|
||||
} catch (e) { alert(e.message); }
|
||||
}
|
||||
|
||||
document.getElementById('btn-add-site')?.addEventListener('click', () => {
|
||||
document.getElementById('site-form-id').value = '';
|
||||
document.getElementById('site-form-nimi').value = '';
|
||||
document.getElementById('site-form-osoite').value = '';
|
||||
document.getElementById('site-form-kaupunki').value = '';
|
||||
document.getElementById('site-form-title').textContent = 'Uusi sijainti';
|
||||
document.getElementById('site-form-container').style.display = '';
|
||||
});
|
||||
|
||||
document.getElementById('btn-save-site')?.addEventListener('click', async () => {
|
||||
const id = document.getElementById('site-form-id').value;
|
||||
const nimi = document.getElementById('site-form-nimi').value.trim();
|
||||
if (!nimi) return alert('Sijainnin nimi vaaditaan');
|
||||
const data = {
|
||||
nimi,
|
||||
osoite: document.getElementById('site-form-osoite').value.trim(),
|
||||
kaupunki: document.getElementById('site-form-kaupunki').value.trim(),
|
||||
};
|
||||
if (id) data.id = id;
|
||||
try {
|
||||
await apiCall('site_save', 'POST', data);
|
||||
document.getElementById('site-form-container').style.display = 'none';
|
||||
loadSites();
|
||||
} catch (e) { alert(e.message); }
|
||||
});
|
||||
|
||||
document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
|
||||
document.getElementById('site-form-container').style.display = 'none';
|
||||
});
|
||||
|
||||
// ==================== MODUULIT ====================
|
||||
|
||||
const ALL_MODULES = ['customers', 'support', 'leads', 'archive', 'changelog', 'settings'];
|
||||
const ALL_MODULES = ['customers', 'support', 'leads', 'devices', 'archive', 'changelog', 'settings'];
|
||||
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
|
||||
|
||||
function applyModules(modules) {
|
||||
|
||||
Reference in New Issue
Block a user