-
-
-
-
-
-
-
-
+ ${tyyppiLabel[e.tyyppi] || e.tyyppi}
+
+ ${e.vlan_id ? '' + e.vlan_id + '' : '-'}
+ ${esc(e.nimi || '-')}
+ ${e.site_name ? esc(e.site_name) : '-'}
+ ${tilaLabel[e.tila] || e.tila}
+ ${esc(e.asiakas || '-')}
+
+
+
+
+ `).join('');
+ }
+ document.getElementById('ipam-count').textContent = filtered.length + ' merkintää' + (query ? ` (${ipamData.length} yhteensä)` : '');
+}
+
+async function loadIpamSitesDropdown() {
+ try {
+ if (!sitesData || sitesData.length === 0) sitesData = await apiCall('sites');
+ const sel = document.getElementById('ipam-form-site');
+ sel.innerHTML = '' +
+ sitesData.map(s => ``).join('');
+ } catch (e) { console.error(e); }
+}
+
+async function editIpam(id) {
+ const e = ipamData.find(x => x.id === id);
+ if (!e) return;
+ document.getElementById('ipam-form-id').value = e.id;
+ document.getElementById('ipam-form-tyyppi').value = e.tyyppi || 'ip';
+ document.getElementById('ipam-form-verkko').value = e.verkko || '';
+ document.getElementById('ipam-form-vlan').value = e.vlan_id || '';
+ document.getElementById('ipam-form-nimi').value = e.nimi || '';
+ document.getElementById('ipam-form-tila').value = e.tila || 'vapaa';
+ document.getElementById('ipam-form-asiakas').value = e.asiakas || '';
+ document.getElementById('ipam-form-lisatiedot').value = e.lisatiedot || '';
+ await loadIpamSitesDropdown();
+ document.getElementById('ipam-form-site').value = e.site_id || '';
+ document.getElementById('ipam-modal-title').textContent = 'Muokkaa IPAM-merkintää';
+ document.getElementById('ipam-modal').style.display = 'flex';
+}
+
+async function deleteIpam(id, name) {
+ if (!confirm(`Poistetaanko IPAM-merkintä "${name}"?`)) return;
+ try {
+ await apiCall('ipam_delete', 'POST', { id });
+ loadIpam();
+ } catch (e) { alert(e.message); }
+}
+
+document.getElementById('btn-add-ipam')?.addEventListener('click', async () => {
+ document.getElementById('ipam-form-id').value = '';
+ document.getElementById('ipam-form').reset();
+ await loadIpamSitesDropdown();
+ document.getElementById('ipam-modal-title').textContent = 'Lisää IPAM-merkintä';
+ document.getElementById('ipam-modal').style.display = 'flex';
+});
+
+document.getElementById('ipam-modal-close')?.addEventListener('click', () => {
+ document.getElementById('ipam-modal').style.display = 'none';
+});
+document.getElementById('ipam-form-cancel')?.addEventListener('click', () => {
+ document.getElementById('ipam-modal').style.display = 'none';
+});
+
+document.getElementById('ipam-form')?.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const id = document.getElementById('ipam-form-id').value;
+ const data = {
+ tyyppi: document.getElementById('ipam-form-tyyppi').value,
+ verkko: document.getElementById('ipam-form-verkko').value.trim(),
+ vlan_id: document.getElementById('ipam-form-vlan').value || null,
+ nimi: document.getElementById('ipam-form-nimi').value.trim(),
+ site_id: document.getElementById('ipam-form-site').value || null,
+ tila: document.getElementById('ipam-form-tila').value,
+ asiakas: document.getElementById('ipam-form-asiakas').value.trim(),
+ lisatiedot: document.getElementById('ipam-form-lisatiedot').value.trim(),
+ };
+ if (id) data.id = id;
+ try {
+ await apiCall('ipam_save', 'POST', data);
+ document.getElementById('ipam-modal').style.display = 'none';
+ loadIpam();
+ } catch (e) { alert(e.message); }
+});
+
+document.getElementById('ipam-search-input')?.addEventListener('input', () => renderIpam());
+
// ==================== MODUULIT ====================
-const ALL_MODULES = ['customers', 'support', 'leads', 'devices', 'archive', 'changelog', 'settings'];
+const ALL_MODULES = ['customers', 'support', 'leads', 'tekniikka', 'archive', 'changelog', 'settings'];
const DEFAULT_MODULES = ['customers', 'support', 'archive', 'changelog', 'settings'];
function applyModules(modules) {
+ // Yhteensopivuus: vanha 'devices' → 'tekniikka'
+ if (modules && modules.includes('devices') && !modules.includes('tekniikka')) {
+ modules = modules.map(m => m === 'devices' ? 'tekniikka' : m);
+ }
// Jos tyhjä array → kaikki moduulit päällä (fallback)
const enabled = (modules && modules.length > 0) ? modules : ALL_MODULES;
const isAdminUser = currentUser?.role === 'admin' || currentUser?.role === 'superadmin';
diff --git a/style.css b/style.css
index 01a25da..8ab39cd 100644
--- a/style.css
+++ b/style.css
@@ -1022,6 +1022,46 @@ span.empty {
display: block;
}
+/* Sub-tabs (Tekniikka yms.) */
+.sub-tab-bar {
+ display: flex;
+ gap: 0;
+ background: #f0f2f5;
+ border-bottom: 2px solid #e0e0e0;
+ padding: 0 1rem;
+}
+.sub-tab {
+ padding: 0.6rem 1.1rem;
+ background: none;
+ border: none;
+ border-bottom: 3px solid transparent;
+ font-size: 0.84rem;
+ font-weight: 600;
+ color: #666;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+.sub-tab:hover {
+ color: var(--primary-color);
+ background: rgba(0,0,0,0.03);
+}
+.sub-tab.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+.sub-tab-content {
+ display: none;
+}
+.sub-tab-content.active {
+ display: block;
+}
+
+/* IPAM tila-badget */
+.ipam-tila { display:inline-block; padding:2px 10px; border-radius:12px; font-size:0.8rem; font-weight:600; }
+.ipam-tila-vapaa { background:#e6f9ee; color:#1a7d42; }
+.ipam-tila-varattu { background:#fef3cd; color:#856404; }
+.ipam-tila-reserved { background:#e2e3e5; color:#495057; }
+
/* Role badge */
.role-badge {
display: inline-block;
| Nimi ↕ | -Hallintaosoite | -Serial | -Sijainti | -Funktio | -Tyyppi | -Malli | -Ping | -Toiminnot | -
|---|
-
+
+ Ei laitteita vielä. Lisää ensimmäinen laite.
+
+
+
+
+
+
+
+
+
+
@@ -656,7 +729,7 @@
Liidit
+
-
+
+
+
+
+
+
+
+
+
+ | Nimi ↕ | +Hallintaosoite | +Serial | +Sijainti | +Funktio | +Tyyppi | +Malli | +Ping | +Toiminnot | +
|---|
+
+ Ei laitteita vielä. Lisää ensimmäinen laite.
+
+ 0 laitetta
- 0 laitetta
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Nimi | +Osoite | +Kaupunki | +Laitteita | +Toiminnot | +
|---|
+
+ Ei sijainteja vielä. Lisää ensimmäinen sijainti.
+
+ 0 sijaintia
+
+
+
+
+
+
+
+
+
+
+
+
+ | Tyyppi | +Verkko / IP | +VLAN | +Nimi / Kuvaus | +Sijainti | +Tila | +Asiakas | +Toiminnot | +
|---|
+
+ Ei IPAM-merkintöjä vielä.
+
+ 0 merkintää
+
+
+
+
+
+
+
+ Lisää IPAM-merkintä
+ +
diff --git a/script.js b/script.js
index b2e9ba8..9f5c6fa 100644
--- a/script.js
+++ b/script.js
@@ -194,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', 'devices', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
+ const validTabs = ['customers', 'leads', 'tekniikka', 'archive', 'changelog', 'support', 'users', 'settings', 'companies'];
const startTab = validTabs.includes(hash) ? hash : 'customers';
switchToTab(startTab);
}
@@ -241,7 +241,7 @@ function switchToTab(target) {
// Lataa sisältö tarvittaessa
if (target === 'customers') loadCustomers();
if (target === 'leads') loadLeads();
- if (target === 'devices') loadDevices();
+ if (target === 'tekniikka') { loadDevices(); loadSitesTab(); loadIpam(); }
if (target === 'archive') loadArchive();
if (target === 'changelog') loadChangelog();
if (target === 'support') { loadTickets(); showTicketListView(); if (document.getElementById('ticket-auto-refresh').checked) startTicketAutoRefresh(); }
@@ -2083,8 +2083,11 @@ async function showCompanyDetail(id) {
logoPreview.style.display = 'none';
}
- // Moduuli-checkboxit
- const enabledMods = comp?.enabled_modules || [];
+ // Moduuli-checkboxit (yhteensopivuus: vanha 'devices' → 'tekniikka')
+ let enabledMods = comp?.enabled_modules || [];
+ if (enabledMods.includes('devices') && !enabledMods.includes('tekniikka')) {
+ enabledMods = enabledMods.map(m => m === 'devices' ? 'tekniikka' : m);
+ }
document.querySelectorAll('#modules-checkboxes input[data-module]').forEach(cb => {
const mod = cb.dataset.module;
// Jos enabled_modules on tyhjä → kaikki päällä (oletus)
@@ -2488,16 +2491,93 @@ document.getElementById('device-form')?.addEventListener('submit', async (e) =>
document.getElementById('device-search-input')?.addEventListener('input', () => renderDevices());
-// ==================== SIJAINNIT (SITES) HALLINTA ====================
+// ==================== TEKNIIKKA SUB-TABS ====================
-async function loadSites() {
+function switchSubTab(target) {
+ document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(t => t.classList.remove('active'));
+ document.querySelectorAll('#tab-content-tekniikka .sub-tab-content').forEach(c => c.classList.remove('active'));
+ const btn = document.querySelector(`.sub-tab[data-subtab="${target}"]`);
+ if (btn) btn.classList.add('active');
+ const content = document.getElementById('subtab-' + target);
+ if (content) content.classList.add('active');
+}
+
+document.querySelectorAll('#tab-content-tekniikka .sub-tab').forEach(btn => {
+ btn.addEventListener('click', () => switchSubTab(btn.dataset.subtab));
+});
+
+// ==================== SIJAINNIT (SITES) — TEKNIIKKA TAB ====================
+
+let sitesTabData = [];
+
+async function loadSitesTab() {
try {
sitesData = await apiCall('sites');
- renderSites();
+ sitesTabData = sitesData;
+ renderSitesTab();
+ renderSitesSettings(); // Päivitä myös asetuksissa
} catch (e) { console.error(e); }
}
-function renderSites() {
+function renderSitesTab() {
+ const query = (document.getElementById('site-search-input')?.value || '').toLowerCase().trim();
+ let filtered = sitesTabData;
+ if (query) {
+ filtered = sitesTabData.filter(s =>
+ (s.nimi || '').toLowerCase().includes(query) ||
+ (s.osoite || '').toLowerCase().includes(query) ||
+ (s.kaupunki || '').toLowerCase().includes(query)
+ );
+ }
+ const tbody = document.getElementById('site-tbody');
+ const noSites = document.getElementById('no-sites-tab');
+ if (filtered.length === 0) {
+ tbody.innerHTML = '';
+ if (noSites) noSites.style.display = 'block';
+ } else {
+ if (noSites) noSites.style.display = 'none';
+ tbody.innerHTML = filtered.map(s => {
+ const deviceCount = devicesData.filter(d => d.site_id === s.id).length;
+ return `
+ ${esc(s.nimi)}
+ ${esc(s.osoite || '-')}
+ ${esc(s.kaupunki || '-')}
+ ${deviceCount}
+
+
+
+
+ `;
+ }).join('');
+ }
+ document.getElementById('site-count').textContent = filtered.length + ' sijaintia';
+}
+
+function editSiteTab(id) {
+ const s = sitesData.find(x => x.id === id);
+ if (!s) return;
+ document.getElementById('site-form-id').value = s.id;
+ document.getElementById('site-form-nimi').value = s.nimi || '';
+ document.getElementById('site-form-osoite').value = s.osoite || '';
+ document.getElementById('site-form-kaupunki').value = s.kaupunki || '';
+ document.getElementById('site-form-title').textContent = 'Muokkaa sijaintia';
+ document.getElementById('site-form-container').style.display = '';
+}
+
+// Alias vanhalle editSite-funktiolle
+function editSite(id) { editSiteTab(id); }
+
+async function deleteSite(id, name) {
+ if (!confirm(`Poistetaanko sijainti "${name}"? Laitteet joissa tämä sijainti on menettävät sijainti-viittauksen.`)) return;
+ try {
+ await apiCall('site_delete', 'POST', { id });
+ loadSitesTab();
+ loadDevices();
+ } catch (e) { alert(e.message); }
+}
+
+// Renderöi sijainnit myös asetuksissa (company detail)
+function renderSitesSettings() {
const container = document.getElementById('sites-list');
if (!container) return;
if (sitesData.length === 0) {
@@ -2517,24 +2597,33 @@ function renderSites() {
`).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 = '';
-}
+// Alias loadSites asetuksista kutsuun
+async function loadSites() { await loadSitesTab(); }
-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); }
-}
+function renderSites() { renderSitesSettings(); }
+
+document.getElementById('site-search-input')?.addEventListener('input', () => renderSitesTab());
+
+// Lisää sijainti -napit (tekniikka-tab + laitteet-sivun quick-nappi)
+document.getElementById('btn-add-site-tab')?.addEventListener('click', () => {
+ document.getElementById('site-form-id').value = '';
+ document.getElementById('site-form-nimi').value = '';
+ document.getElementById('site-form-osoite').value = '';
+ document.getElementById('site-form-kaupunki').value = '';
+ document.getElementById('site-form-title').textContent = 'Uusi sijainti';
+ document.getElementById('site-form-container').style.display = '';
+ switchSubTab('sites');
+});
+
+document.getElementById('btn-add-site-quick')?.addEventListener('click', () => {
+ document.getElementById('site-form-id').value = '';
+ document.getElementById('site-form-nimi').value = '';
+ document.getElementById('site-form-osoite').value = '';
+ document.getElementById('site-form-kaupunki').value = '';
+ document.getElementById('site-form-title').textContent = 'Uusi sijainti';
+ document.getElementById('site-form-container').style.display = '';
+ switchSubTab('sites');
+});
document.getElementById('btn-add-site')?.addEventListener('click', () => {
document.getElementById('site-form-id').value = '';
@@ -2558,7 +2647,7 @@ document.getElementById('btn-save-site')?.addEventListener('click', async () =>
try {
await apiCall('site_save', 'POST', data);
document.getElementById('site-form-container').style.display = 'none';
- loadSites();
+ loadSitesTab();
} catch (e) { alert(e.message); }
});
@@ -2566,12 +2655,140 @@ document.getElementById('btn-cancel-site')?.addEventListener('click', () => {
document.getElementById('site-form-container').style.display = 'none';
});
+// ==================== IPAM ====================
+
+let ipamData = [];
+
+async function loadIpam() {
+ try {
+ ipamData = await apiCall('ipam');
+ renderIpam();
+ } catch (e) { console.error(e); }
+}
+
+function renderIpam() {
+ const query = (document.getElementById('ipam-search-input')?.value || '').toLowerCase().trim();
+ let filtered = ipamData;
+ if (query) {
+ filtered = ipamData.filter(e =>
+ (e.tyyppi || '').toLowerCase().includes(query) ||
+ (e.verkko || '').toLowerCase().includes(query) ||
+ (e.nimi || '').toLowerCase().includes(query) ||
+ (e.site_name || '').toLowerCase().includes(query) ||
+ (e.asiakas || '').toLowerCase().includes(query) ||
+ (e.lisatiedot || '').toLowerCase().includes(query) ||
+ String(e.vlan_id || '').includes(query)
+ );
+ }
+ const tbody = document.getElementById('ipam-tbody');
+ const noIpam = document.getElementById('no-ipam');
+ if (filtered.length === 0) {
+ tbody.innerHTML = '';
+ if (noIpam) noIpam.style.display = 'block';
+ } else {
+ if (noIpam) noIpam.style.display = 'none';
+ const tyyppiLabel = { subnet: 'Subnet', vlan: 'VLAN', ip: 'IP' };
+ const tilaClass = { vapaa: 'ipam-tila-vapaa', varattu: 'ipam-tila-varattu', reserved: 'ipam-tila-reserved' };
+ const tilaLabel = { vapaa: 'Vapaa', varattu: 'Varattu', reserved: 'Reserved' };
+ tbody.innerHTML = filtered.map(e => `${esc(e.verkko || '-')}