Liittymien VLAN/Laite/IP-kentät hakukentiksi + IP/verkko-tuki
- Korvattu select-dropdownit hakukentillä (searchable combobox) - Kirjoittamalla suodattaa tuloksia nimellä, IP:llä, sijainnilla jne. - Nuolinäppäimillä navigointi, Enter valitsee, Esc sulkee - Vapaan tekstin syöttö mahdollista jos IPAM:sta ei löydy - IP-kenttä tukee nyt myös verkkoja (subnet/prefix) IP-osoitteiden lisäksi - Vapaat IP:t, varatut IP:t ja verkot ryhmitelty omiin osioihinsa - Badge-värit: vihreä (vapaa), punainen (varattu), sininen (subnet) - Sama hakukenttä-komponentti sekä netadmin-modalissa että asiakasformissa - API palauttaa nyt subnetit IP-listan mukana Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
4
api.php
4
api.php
@@ -4412,10 +4412,10 @@ switch ($action) {
|
|||||||
$deviceMap = [];
|
$deviceMap = [];
|
||||||
foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; }
|
foreach ($devices as $d) { $deviceMap[$d['nimi']] = $d; }
|
||||||
|
|
||||||
// Hae IPAM VLANit ja IP:t dropdown-valikkoja varten
|
// Hae IPAM VLANit, IP:t ja subnetit valikkoja varten
|
||||||
$ipamAll = dbLoadIpam($companyId);
|
$ipamAll = dbLoadIpam($companyId);
|
||||||
$vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan'));
|
$vlans = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'vlan'));
|
||||||
$ips = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'ip'));
|
$ips = array_values(array_filter($ipamAll, fn($e) => $e['tyyppi'] === 'ip' || $e['tyyppi'] === 'subnet'));
|
||||||
|
|
||||||
// Rikasta liittymädata laitetiedoilla
|
// Rikasta liittymädata laitetiedoilla
|
||||||
foreach ($connections as &$conn) {
|
foreach ($connections as &$conn) {
|
||||||
|
|||||||
26
index.html
26
index.html
@@ -1105,25 +1105,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>VLAN</label>
|
<label>VLAN</label>
|
||||||
<select id="na-edit-vlan">
|
<div class="combo-wrap" id="na-combo-vlan">
|
||||||
<option value="">- Ei VLANia -</option>
|
<input type="text" placeholder="Hae VLANia..." autocomplete="off">
|
||||||
</select>
|
<input type="hidden" id="na-edit-vlan">
|
||||||
|
<div class="combo-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Laite</label>
|
<label>Laite</label>
|
||||||
<select id="na-edit-laite">
|
<div class="combo-wrap" id="na-combo-laite">
|
||||||
<option value="">- Ei laitetta -</option>
|
<input type="text" placeholder="Hae laitetta..." autocomplete="off">
|
||||||
</select>
|
<input type="hidden" id="na-edit-laite">
|
||||||
|
<div class="combo-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Portti</label>
|
<label>Portti</label>
|
||||||
<input type="text" id="na-edit-portti">
|
<input type="text" id="na-edit-portti">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>IP</label>
|
<label>IP / Verkko</label>
|
||||||
<select id="na-edit-ip">
|
<div class="combo-wrap" id="na-combo-ip">
|
||||||
<option value="">- Ei IP:tä -</option>
|
<input type="text" placeholder="Hae IP:tä tai verkkoa..." autocomplete="off">
|
||||||
</select>
|
<input type="hidden" id="na-edit-ip">
|
||||||
|
<div class="combo-list"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;">
|
<div style="margin-top:1rem;display:flex;gap:0.5rem;justify-content:flex-end;">
|
||||||
|
|||||||
303
script.js
303
script.js
@@ -684,68 +684,25 @@ function createLiittymaRow(data = {}, index = 0) {
|
|||||||
<option value="36" ${data.sopimuskausi === '36' ? 'selected' : ''}>36 kk</option>
|
<option value="36" ${data.sopimuskausi === '36' ? 'selected' : ''}>36 kk</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
<div class="form-group"><label>Alkaen</label><input type="date" class="l-alkupvm" value="${esc(data.alkupvm || '')}"></div>
|
<div class="form-group"><label>Alkaen</label><input type="date" class="l-alkupvm" value="${esc(data.alkupvm || '')}"></div>
|
||||||
<div class="form-group"><label>VLAN</label><select class="l-vlan"><option value="">- Ei VLANia -</option></select></div>
|
<div class="form-group"><label>VLAN</label><div class="combo-wrap l-combo-vlan"><input type="text" placeholder="Hae VLANia..." autocomplete="off"><input type="hidden" class="l-vlan"><div class="combo-list"></div></div></div>
|
||||||
<div class="form-group"><label>Laite</label><select class="l-laite"><option value="">- Ei laitetta -</option></select></div>
|
<div class="form-group"><label>Laite</label><div class="combo-wrap l-combo-laite"><input type="text" placeholder="Hae laitetta..." autocomplete="off"><input type="hidden" class="l-laite"><div class="combo-list"></div></div></div>
|
||||||
<div class="form-group"><label>Portti</label><input type="text" class="l-portti" value="${esc(data.portti || '')}" placeholder="esim. Gi0/1"></div>
|
<div class="form-group"><label>Portti</label><input type="text" class="l-portti" value="${esc(data.portti || '')}" placeholder="esim. Gi0/1"></div>
|
||||||
<div class="form-group"><label>IP</label><select class="l-ip"><option value="">- Ei IP:tä -</option></select></div>
|
<div class="form-group"><label>IP / Verkko</label><div class="combo-wrap l-combo-ip"><input type="text" placeholder="Hae IP:tä..." autocomplete="off"><input type="hidden" class="l-ip"><div class="combo-list"></div></div></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); });
|
div.querySelector('.btn-remove-row').addEventListener('click', () => { div.remove(); renumberLiittymaRows(); });
|
||||||
// Populoi dropdownit IPAM/laite-datasta
|
// Populoi hakukentät IPAM/laite-datasta
|
||||||
populateLiittymaRowDropdowns(div, data);
|
populateLiittymaRowCombos(div, data);
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populoi yksittäisen liittymärivin VLAN/Laite/IP dropdownit
|
// Populoi liittymärivin comboboxit
|
||||||
function populateLiittymaRowDropdowns(row, data = {}) {
|
function populateLiittymaRowCombos(row, data = {}) {
|
||||||
const vlanSel = row.querySelector('.l-vlan');
|
|
||||||
const laiteSel = row.querySelector('.l-laite');
|
|
||||||
const ipSel = row.querySelector('.l-ip');
|
|
||||||
// Käytä netadminData:a tai fallback ipamData/devicesData:an
|
|
||||||
const vlans = (netadminData.vlans && netadminData.vlans.length) ? netadminData.vlans : (ipamData || []).filter(e => e.tyyppi === 'vlan');
|
const vlans = (netadminData.vlans && netadminData.vlans.length) ? netadminData.vlans : (ipamData || []).filter(e => e.tyyppi === 'vlan');
|
||||||
const ips = (netadminData.ips && netadminData.ips.length) ? netadminData.ips : (ipamData || []).filter(e => e.tyyppi === 'ip');
|
const ips = (netadminData.ips && netadminData.ips.length) ? netadminData.ips : (ipamData || []).filter(e => e.tyyppi === 'ip' || e.tyyppi === 'subnet');
|
||||||
const devices = (netadminData.devices && netadminData.devices.length) ? netadminData.devices : (devicesData || []);
|
const devices = (netadminData.devices && netadminData.devices.length) ? netadminData.devices : (devicesData || []);
|
||||||
// VLAN
|
initCombo(row.querySelector('.l-combo-vlan'), getVlanComboOptions(vlans), data.vlan || '');
|
||||||
let vlanHtml = '<option value="">- Ei VLANia -</option>';
|
initCombo(row.querySelector('.l-combo-laite'), getDeviceComboOptions(devices), data.laite || '');
|
||||||
[...vlans].sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0)).forEach(v => {
|
initCombo(row.querySelector('.l-combo-ip'), getIpComboOptions(ips), data.ip || '');
|
||||||
const val = String(v.vlan_id || '');
|
|
||||||
const label = val + (v.nimi ? ` — ${v.nimi}` : '');
|
|
||||||
vlanHtml += `<option value="${esc(val)}">${esc(label)}</option>`;
|
|
||||||
});
|
|
||||||
if (data.vlan && !vlans.some(v => String(v.vlan_id) === String(data.vlan))) {
|
|
||||||
vlanHtml += `<option value="${esc(data.vlan)}">${esc(data.vlan)} (manuaalinen)</option>`;
|
|
||||||
}
|
|
||||||
vlanSel.innerHTML = vlanHtml;
|
|
||||||
vlanSel.value = data.vlan || '';
|
|
||||||
// Laite
|
|
||||||
let laiteHtml = '<option value="">- Ei laitetta -</option>';
|
|
||||||
[...devices].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || '')).forEach(d => {
|
|
||||||
const label = d.nimi + (d.hallintaosoite ? ` (${d.hallintaosoite})` : '');
|
|
||||||
laiteHtml += `<option value="${esc(d.nimi)}">${esc(label)}</option>`;
|
|
||||||
});
|
|
||||||
if (data.laite && !devices.some(d => d.nimi === data.laite)) {
|
|
||||||
laiteHtml += `<option value="${esc(data.laite)}">${esc(data.laite)} (manuaalinen)</option>`;
|
|
||||||
}
|
|
||||||
laiteSel.innerHTML = laiteHtml;
|
|
||||||
laiteSel.value = data.laite || '';
|
|
||||||
// IP
|
|
||||||
let ipHtml = '<option value="">- Ei IP:tä -</option>';
|
|
||||||
const free = ips.filter(i => i.tila === 'vapaa').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
|
||||||
const taken = ips.filter(i => i.tila === 'varattu').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
|
||||||
if (free.length) {
|
|
||||||
ipHtml += '<optgroup label="Vapaat IP:t">';
|
|
||||||
free.forEach(i => { ipHtml += `<option value="${esc(i.verkko)}">${esc(i.verkko)}${i.nimi ? ` — ${esc(i.nimi)}` : ''}</option>`; });
|
|
||||||
ipHtml += '</optgroup>';
|
|
||||||
}
|
|
||||||
if (taken.length) {
|
|
||||||
ipHtml += '<optgroup label="Varatut IP:t">';
|
|
||||||
taken.forEach(i => { ipHtml += `<option value="${esc(i.verkko)}">${esc(i.verkko)}${i.asiakas ? ` ← ${esc(i.asiakas)}` : ''}</option>`; });
|
|
||||||
ipHtml += '</optgroup>';
|
|
||||||
}
|
|
||||||
if (data.ip && !ips.some(i => i.verkko === data.ip)) {
|
|
||||||
ipHtml += `<option value="${esc(data.ip)}">${esc(data.ip)} (manuaalinen)</option>`;
|
|
||||||
}
|
|
||||||
ipSel.innerHTML = ipHtml;
|
|
||||||
ipSel.value = data.ip || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renumberLiittymaRows() {
|
function renumberLiittymaRows() {
|
||||||
@@ -764,10 +721,10 @@ function collectLiittymatFromForm() {
|
|||||||
hinta: row.querySelector('.l-hinta').value,
|
hinta: row.querySelector('.l-hinta').value,
|
||||||
sopimuskausi: row.querySelector('.l-sopimuskausi').value,
|
sopimuskausi: row.querySelector('.l-sopimuskausi').value,
|
||||||
alkupvm: row.querySelector('.l-alkupvm').value,
|
alkupvm: row.querySelector('.l-alkupvm').value,
|
||||||
vlan: row.querySelector('.l-vlan').value,
|
vlan: row.querySelector('.l-vlan')?.value || '',
|
||||||
laite: row.querySelector('.l-laite').value,
|
laite: row.querySelector('.l-laite')?.value || '',
|
||||||
portti: row.querySelector('.l-portti').value,
|
portti: row.querySelector('.l-portti').value,
|
||||||
ip: row.querySelector('.l-ip').value,
|
ip: row.querySelector('.l-ip')?.value || '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4692,72 +4649,176 @@ document.getElementById('netadmin-filter-city')?.addEventListener('change', rend
|
|||||||
document.getElementById('netadmin-filter-speed')?.addEventListener('change', renderNetadminTable);
|
document.getElementById('netadmin-filter-speed')?.addEventListener('change', renderNetadminTable);
|
||||||
document.getElementById('netadmin-filter-device')?.addEventListener('change', renderNetadminTable);
|
document.getElementById('netadmin-filter-device')?.addEventListener('change', renderNetadminTable);
|
||||||
|
|
||||||
// Populoi VLAN-dropdown IPAM VLANeista
|
// ---- Searchable Combobox ----
|
||||||
function populateVlanDropdown(selectEl, currentValue) {
|
|
||||||
const vlans = netadminData.vlans || [];
|
// Luo combobox hakukenttä wrap-elementin sisälle
|
||||||
let html = '<option value="">- Ei VLANia -</option>';
|
// options: [{value, label, sub, badge, badgeClass, searchStr}]
|
||||||
vlans.sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0));
|
function initCombo(wrapEl, options, currentValue) {
|
||||||
vlans.forEach(v => {
|
const input = wrapEl.querySelector('input[type="text"]');
|
||||||
const val = String(v.vlan_id || '');
|
const hidden = wrapEl.querySelector('input[type="hidden"]');
|
||||||
const label = val + (v.nimi ? ` — ${v.nimi}` : '') + (v.site_name ? ` (${v.site_name})` : '');
|
const list = wrapEl.querySelector('.combo-list');
|
||||||
html += `<option value="${esc(val)}">${esc(label)}</option>`;
|
if (!input || !hidden || !list) return;
|
||||||
});
|
|
||||||
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
|
wrapEl._comboOptions = options;
|
||||||
if (currentValue && !vlans.some(v => String(v.vlan_id) === String(currentValue))) {
|
hidden.value = currentValue || '';
|
||||||
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
|
|
||||||
}
|
// Näytä nykyinen arvo inputissa
|
||||||
selectEl.innerHTML = html;
|
if (currentValue) {
|
||||||
selectEl.value = currentValue || '';
|
const match = options.find(o => o.value === currentValue);
|
||||||
|
input.value = match ? match.label : currentValue;
|
||||||
|
} else {
|
||||||
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populoi laite-dropdown
|
function renderList(query) {
|
||||||
function populateDeviceDropdown(selectEl, currentValue) {
|
const q = (query || '').toLowerCase().trim();
|
||||||
const devices = netadminData.devices || [];
|
let filtered = options;
|
||||||
let html = '<option value="">- Ei laitetta -</option>';
|
if (q) {
|
||||||
devices.sort((a, b) => (a.nimi || '').localeCompare(b.nimi || ''));
|
filtered = options.filter(o =>
|
||||||
devices.forEach(d => {
|
(o.searchStr || o.label || '').toLowerCase().includes(q) ||
|
||||||
const label = d.nimi + (d.hallintaosoite ? ` (${d.hallintaosoite})` : '') + (d.malli ? ` — ${d.malli}` : '');
|
(o.value || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
list.innerHTML = '<div class="combo-opt" style="color:#aaa;cursor:default;">Ei tuloksia</div>';
|
||||||
|
} else {
|
||||||
|
let lastGroup = null;
|
||||||
|
list.innerHTML = filtered.map(o => {
|
||||||
|
let grpHtml = '';
|
||||||
|
if (o.group && o.group !== lastGroup) {
|
||||||
|
lastGroup = o.group;
|
||||||
|
grpHtml = `<div class="combo-grp">${esc(o.group)}</div>`;
|
||||||
|
}
|
||||||
|
const badge = o.badge ? `<span class="combo-badge ${o.badgeClass || ''}">${esc(o.badge)}</span>` : '';
|
||||||
|
const sub = o.sub ? `<span class="combo-sub">${esc(o.sub)}</span>` : '';
|
||||||
|
return grpHtml + `<div class="combo-opt" data-value="${esc(o.value)}">${badge}<span>${esc(o.label)}</span>${sub}</div>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
list.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectValue(val) {
|
||||||
|
hidden.value = val;
|
||||||
|
const match = options.find(o => o.value === val);
|
||||||
|
input.value = match ? match.label : val;
|
||||||
|
list.classList.remove('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poista vanhat listenerit (uudelleeninitiin)
|
||||||
|
const newInput = input.cloneNode(true);
|
||||||
|
input.parentNode.replaceChild(newInput, input);
|
||||||
|
const newList = list.cloneNode(true);
|
||||||
|
list.parentNode.replaceChild(newList, list);
|
||||||
|
|
||||||
|
newInput.addEventListener('focus', () => renderList(newInput.value));
|
||||||
|
newInput.addEventListener('input', () => {
|
||||||
|
renderList(newInput.value);
|
||||||
|
// Jos tyhjä, tyhjennä valinta
|
||||||
|
if (!newInput.value.trim()) hidden.value = '';
|
||||||
|
});
|
||||||
|
newInput.addEventListener('blur', () => {
|
||||||
|
// Pieni viive jotta klikkaus ehtii rekisteröityä
|
||||||
|
setTimeout(() => {
|
||||||
|
newList.classList.remove('open');
|
||||||
|
// Jos input ei vastaa mitään optiota, käytä vapaa teksti arvona
|
||||||
|
if (newInput.value.trim() && !hidden.value) {
|
||||||
|
hidden.value = newInput.value.trim();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
newInput.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') { newList.classList.remove('open'); newInput.blur(); }
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
const active = newList.querySelector('.combo-opt.active');
|
||||||
|
if (active && active.dataset.value !== undefined) {
|
||||||
|
selectValue(active.dataset.value);
|
||||||
|
} else {
|
||||||
|
// Valitse ensimmäinen tulos
|
||||||
|
const first = newList.querySelector('.combo-opt[data-value]');
|
||||||
|
if (first) selectValue(first.dataset.value);
|
||||||
|
else { hidden.value = newInput.value.trim(); newList.classList.remove('open'); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
const items = [...newList.querySelectorAll('.combo-opt[data-value]')];
|
||||||
|
if (!items.length) return;
|
||||||
|
const idx = items.findIndex(i => i.classList.contains('active'));
|
||||||
|
items.forEach(i => i.classList.remove('active'));
|
||||||
|
let next = e.key === 'ArrowDown' ? idx + 1 : idx - 1;
|
||||||
|
if (next < 0) next = items.length - 1;
|
||||||
|
if (next >= items.length) next = 0;
|
||||||
|
items[next].classList.add('active');
|
||||||
|
items[next].scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
newList.addEventListener('mousedown', (e) => {
|
||||||
|
const opt = e.target.closest('.combo-opt[data-value]');
|
||||||
|
if (opt) { e.preventDefault(); selectValue(opt.dataset.value); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rakennetaan VLAN-combobox optiot
|
||||||
|
function getVlanComboOptions(source) {
|
||||||
|
const vlans = source || netadminData.vlans || [];
|
||||||
|
return [...vlans].sort((a, b) => (a.vlan_id || 0) - (b.vlan_id || 0)).map(v => ({
|
||||||
|
value: String(v.vlan_id || ''),
|
||||||
|
label: String(v.vlan_id || '') + (v.nimi ? ` — ${v.nimi}` : ''),
|
||||||
|
sub: v.site_name || '',
|
||||||
|
searchStr: `${v.vlan_id} ${v.nimi || ''} ${v.site_name || ''}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rakennetaan laite-combobox optiot
|
||||||
|
function getDeviceComboOptions(source) {
|
||||||
|
const devices = source || netadminData.devices || [];
|
||||||
|
return [...devices].sort((a, b) => (a.nimi || '').localeCompare(b.nimi || '')).map(d => {
|
||||||
const pingDot = d.ping_status === 'up' ? '🟢' : d.ping_status === 'down' ? '🔴' : '';
|
const pingDot = d.ping_status === 'up' ? '🟢' : d.ping_status === 'down' ? '🔴' : '';
|
||||||
html += `<option value="${esc(d.nimi)}">${pingDot}${esc(label)}</option>`;
|
return {
|
||||||
|
value: d.nimi,
|
||||||
|
label: (pingDot ? pingDot + ' ' : '') + d.nimi,
|
||||||
|
sub: [d.hallintaosoite, d.malli].filter(Boolean).join(' — '),
|
||||||
|
searchStr: `${d.nimi} ${d.hallintaosoite || ''} ${d.malli || ''} ${d.funktio || ''} ${d.site_name || ''}`,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
|
|
||||||
if (currentValue && !devices.some(d => d.nimi === currentValue)) {
|
|
||||||
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
|
|
||||||
}
|
|
||||||
selectEl.innerHTML = html;
|
|
||||||
selectEl.value = currentValue || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populoi IP-dropdown IPAM IP-osoitteista
|
// Rakennetaan IP/verkko -combobox optiot
|
||||||
function populateIpDropdown(selectEl, currentValue) {
|
function getIpComboOptions(source) {
|
||||||
const ips = netadminData.ips || [];
|
const ips = source || netadminData.ips || [];
|
||||||
let html = '<option value="">- Ei IP:tä -</option>';
|
const items = [];
|
||||||
// Ryhmittele: vapaat ensin, sitten varatut
|
// Vapaat IP:t
|
||||||
const free = ips.filter(i => i.tila === 'vapaa').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
const free = ips.filter(i => i.tila === 'vapaa' && i.tyyppi === 'ip').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
||||||
const taken = ips.filter(i => i.tila === 'varattu').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
free.forEach(i => items.push({
|
||||||
|
value: i.verkko,
|
||||||
if (free.length) {
|
label: i.verkko,
|
||||||
html += '<optgroup label="Vapaat IP:t">';
|
sub: [i.nimi, i.site_name].filter(Boolean).join(' — '),
|
||||||
free.forEach(i => {
|
badge: 'vapaa', badgeClass: 'free',
|
||||||
const label = i.verkko + (i.nimi ? ` — ${i.nimi}` : '') + (i.site_name ? ` (${i.site_name})` : '');
|
group: 'Vapaat IP:t',
|
||||||
html += `<option value="${esc(i.verkko)}">${esc(label)}</option>`;
|
searchStr: `${i.verkko} ${i.nimi || ''} ${i.site_name || ''}`,
|
||||||
});
|
}));
|
||||||
html += '</optgroup>';
|
// Varatut IP:t
|
||||||
}
|
const taken = ips.filter(i => i.tila === 'varattu' && i.tyyppi === 'ip').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
||||||
if (taken.length) {
|
taken.forEach(i => items.push({
|
||||||
html += '<optgroup label="Varatut IP:t">';
|
value: i.verkko,
|
||||||
taken.forEach(i => {
|
label: i.verkko,
|
||||||
const label = i.verkko + (i.asiakas ? ` ← ${i.asiakas}` : '') + (i.nimi ? ` — ${i.nimi}` : '');
|
sub: [i.asiakas, i.nimi].filter(Boolean).join(' — '),
|
||||||
html += `<option value="${esc(i.verkko)}">${esc(label)}</option>`;
|
badge: 'varattu', badgeClass: 'taken',
|
||||||
});
|
group: 'Varatut IP:t',
|
||||||
html += '</optgroup>';
|
searchStr: `${i.verkko} ${i.nimi || ''} ${i.asiakas || ''} ${i.site_name || ''}`,
|
||||||
}
|
}));
|
||||||
// Jos nykyinen arvo ei ole listalla, lisää se custom-optiona
|
// Subnetit
|
||||||
if (currentValue && !ips.some(i => i.verkko === currentValue)) {
|
const subnets = ips.filter(i => i.tyyppi === 'subnet').sort((a, b) => (a.verkko || '').localeCompare(b.verkko || ''));
|
||||||
html += `<option value="${esc(currentValue)}">${esc(currentValue)} (manuaalinen)</option>`;
|
subnets.forEach(i => items.push({
|
||||||
}
|
value: i.verkko,
|
||||||
selectEl.innerHTML = html;
|
label: i.verkko,
|
||||||
selectEl.value = currentValue || '';
|
sub: [i.nimi, i.site_name].filter(Boolean).join(' — '),
|
||||||
|
badge: 'subnet', badgeClass: 'subnet',
|
||||||
|
group: 'Verkot',
|
||||||
|
searchStr: `${i.verkko} ${i.nimi || ''} ${i.site_name || ''}`,
|
||||||
|
}));
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openNetadminDetail(connId) {
|
async function openNetadminDetail(connId) {
|
||||||
@@ -4779,10 +4840,10 @@ async function openNetadminDetail(connId) {
|
|||||||
speedSel.insertBefore(opt, speedSel.lastElementChild);
|
speedSel.insertBefore(opt, speedSel.lastElementChild);
|
||||||
}
|
}
|
||||||
speedSel.value = speed;
|
speedSel.value = speed;
|
||||||
// Populoi VLAN, Laite ja IP dropdownit IPAM/Tekniikka-datasta
|
// Populoi VLAN, Laite ja IP hakukentät IPAM/Tekniikka-datasta
|
||||||
populateVlanDropdown(document.getElementById('na-edit-vlan'), conn.vlan || '');
|
initCombo(document.getElementById('na-combo-vlan'), getVlanComboOptions(), conn.vlan || '');
|
||||||
populateDeviceDropdown(document.getElementById('na-edit-laite'), conn.laite || '');
|
initCombo(document.getElementById('na-combo-laite'), getDeviceComboOptions(), conn.laite || '');
|
||||||
populateIpDropdown(document.getElementById('na-edit-ip'), conn.ip || '');
|
initCombo(document.getElementById('na-combo-ip'), getIpComboOptions(), conn.ip || '');
|
||||||
document.getElementById('na-edit-portti').value = conn.portti || '';
|
document.getElementById('na-edit-portti').value = conn.portti || '';
|
||||||
document.getElementById('netadmin-detail-modal').style.display = '';
|
document.getElementById('netadmin-detail-modal').style.display = '';
|
||||||
} catch (e) { alert('Liittymän avaus epäonnistui: ' + e.message); }
|
} catch (e) { alert('Liittymän avaus epäonnistui: ' + e.message); }
|
||||||
|
|||||||
29
style.css
29
style.css
@@ -1820,3 +1820,32 @@ span.empty {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Searchable combobox */
|
||||||
|
.combo-wrap { position: relative; }
|
||||||
|
.combo-wrap input {
|
||||||
|
width: 100%; box-sizing: border-box;
|
||||||
|
padding: 0.5rem 0.7rem; border: 1px solid #d1d5db; border-radius: 6px;
|
||||||
|
font-size: 0.9rem; background: #fff;
|
||||||
|
}
|
||||||
|
.combo-wrap input:focus { border-color: var(--primary-color); outline: none; box-shadow: 0 0 0 2px rgba(59,130,246,.15); }
|
||||||
|
.combo-list {
|
||||||
|
display: none; position: absolute; left: 0; right: 0; top: 100%;
|
||||||
|
max-height: 220px; overflow-y: auto; z-index: 100;
|
||||||
|
background: #fff; border: 1px solid #d1d5db; border-top: none;
|
||||||
|
border-radius: 0 0 8px 8px; box-shadow: 0 4px 12px rgba(0,0,0,.1);
|
||||||
|
}
|
||||||
|
.combo-list.open { display: block; }
|
||||||
|
.combo-opt {
|
||||||
|
padding: 0.4rem 0.7rem; cursor: pointer; font-size: 0.85rem;
|
||||||
|
display: flex; align-items: center; gap: 0.4rem;
|
||||||
|
}
|
||||||
|
.combo-opt:hover, .combo-opt.active { background: #eff6ff; }
|
||||||
|
.combo-opt .combo-sub { font-size: 0.78rem; color: #888; margin-left: auto; white-space: nowrap; }
|
||||||
|
.combo-opt .combo-badge {
|
||||||
|
font-size: 0.7rem; padding: 1px 5px; border-radius: 4px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.combo-badge.free { background: #dcfce7; color: #166534; }
|
||||||
|
.combo-badge.taken { background: #fee2e2; color: #991b1b; }
|
||||||
|
.combo-badge.subnet { background: #e0e7ff; color: #3730a3; }
|
||||||
|
.combo-grp { padding: 0.3rem 0.7rem; font-size: 0.75rem; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: 0.03em; }
|
||||||
|
|||||||
Reference in New Issue
Block a user