Add gate groups. Graphics adjustments
This commit is contained in:
@@ -158,6 +158,7 @@
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Group</th>
|
||||
<th>Type</th>
|
||||
<th>AVConnect Macro ID</th>
|
||||
<th>Status</th>
|
||||
@@ -323,6 +324,11 @@
|
||||
<label for="gate-name">Name</label>
|
||||
<input id="gate-name" type="text" placeholder="e.g. Main entrance - Car" required />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="gate-group-name">Group <span style="color:var(--text-muted);font-weight:400">(optional)</span></label>
|
||||
<input id="gate-group-name" type="text" placeholder="e.g. Main entrance" list="gate-group-list" autocomplete="off" />
|
||||
<datalist id="gate-group-list"></datalist>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="gate-type">Type</label>
|
||||
<select id="gate-type">
|
||||
|
||||
@@ -141,12 +141,12 @@ async function loadKeypasses() {
|
||||
<td>${gatesCell}</td>
|
||||
<td>${expiresCell}</td>
|
||||
<td>${badge}</td>
|
||||
<td style="text-align:right">
|
||||
<td><div style="display:flex;gap:.5rem;justify-content:flex-end;white-space:nowrap">
|
||||
${!kp.revoked ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-edit-kp='${JSON.stringify({id:kp.id, description:kp.description, expires_at:kp.expires_at, allowed_gate_ids:kp.allowed_gate_ids})}'>Edit</button>` : ""}
|
||||
${!kp.revoked && expMs >= now ? `<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-kp-id="${kp.id}">Revoke</button>` : ""}
|
||||
</td>`;
|
||||
</div></td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
@@ -345,17 +345,18 @@ async function loadGates() {
|
||||
tr.innerHTML = `
|
||||
<td>${g.id}</td>
|
||||
<td>${esc(g.name)}</td>
|
||||
<td>${g.group_name ? esc(g.group_name) : '<span style="color:var(--text-muted)">—</span>'}</td>
|
||||
<td>${g.gate_type === "car" ? "🚘 Car" : "🚶 Pedestrian"}</td>
|
||||
<td><code style="font-size:.85em">${esc(g.avconnect_macro_id)}</code></td>
|
||||
<td>${badge}</td>
|
||||
<td style="text-align:right;white-space:nowrap;display:flex;gap:.5rem;justify-content:flex-end">
|
||||
<td><div style="text-align:right;white-space:nowrap;display:flex;gap:.5rem;justify-content:flex-end">
|
||||
${g.status === 'enabled' ? `<button class="btn btn-primary" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-open-id="${g.id}">Open</button>` : ''}
|
||||
${isAdmin ? `<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-edit-id="${g.id}" data-gate='${JSON.stringify(g)}'>Edit</button>
|
||||
<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem"
|
||||
data-del-id="${g.id}">Delete</button>` : ''}
|
||||
</td>`;
|
||||
</div></td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
@@ -390,10 +391,20 @@ function openGateModal(gate = null) {
|
||||
document.getElementById("gate-modal-title").textContent = gate ? "Edit Gate" : "Add Gate";
|
||||
document.getElementById("gate-edit-id").value = gate ? gate.id : "";
|
||||
document.getElementById("gate-name").value = gate ? gate.name : "";
|
||||
document.getElementById("gate-group-name").value = gate ? (gate.group_name || "") : "";
|
||||
document.getElementById("gate-type").value = gate ? gate.gate_type : "car";
|
||||
document.getElementById("gate-avconnect-macro-id").value = gate ? gate.avconnect_macro_id : "";
|
||||
document.getElementById("gate-status").value = gate ? gate.status : "enabled";
|
||||
document.getElementById("gate-error").classList.add("hidden");
|
||||
// Populate group suggestions from existing gates
|
||||
const dl = document.getElementById("gate-group-list");
|
||||
dl.innerHTML = "";
|
||||
const groups = [...new Set((_allGates || []).map(g => g.group_name).filter(Boolean))].sort();
|
||||
for (const g of groups) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = g;
|
||||
dl.appendChild(opt);
|
||||
}
|
||||
document.getElementById("gate-modal").classList.remove("hidden");
|
||||
}
|
||||
|
||||
@@ -409,6 +420,7 @@ document.getElementById("gate-form").addEventListener("submit", async e => {
|
||||
gate_type: document.getElementById("gate-type").value,
|
||||
avconnect_macro_id: document.getElementById("gate-avconnect-macro-id").value.trim(),
|
||||
status: document.getElementById("gate-status").value,
|
||||
group_name: document.getElementById("gate-group-name").value.trim() || null,
|
||||
};
|
||||
const errEl = document.getElementById("gate-error");
|
||||
errEl.classList.add("hidden");
|
||||
@@ -505,11 +517,11 @@ async function loadAdmins() {
|
||||
: '<span class="badge badge-muted" style="font-size:.75em">manager</span>';
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML = `
|
||||
<td>${esc(u.username)}${u.username === me ? ' <span class="badge badge-green" style="font-size:.75em">you</span>' : ""} ${roleBadge}</td>
|
||||
<td style="text-align:right;display:flex;gap:.5rem;justify-content:flex-end">
|
||||
<td><div style="display:flex;align-items:center;gap:.4rem;flex-wrap:nowrap">${esc(u.username)}${u.username === me ? '<span class="badge badge-green" style="font-size:.75em">you</span>' : ""} ${roleBadge}</div></td>
|
||||
<td><div style="text-align:right;display:flex;gap:.5rem;justify-content:flex-end">
|
||||
<button class="btn btn-ghost" style="font-size:.8rem;padding:.35rem .9rem" data-chpw="${esc(u.username)}">Change password</button>
|
||||
${u.username !== me ? `<button class="btn btn-danger" style="font-size:.8rem;padding:.35rem .9rem" data-del-admin="${esc(u.username)}">Delete</button>` : ""}
|
||||
</td>`;
|
||||
</div></td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
tbody.querySelectorAll("[data-chpw]").forEach(btn => {
|
||||
|
||||
@@ -64,15 +64,46 @@ function renderGates(gates) {
|
||||
loading.classList.add("hidden");
|
||||
grid.classList.remove("hidden");
|
||||
|
||||
// Group gates by group_name; null/empty = ungrouped (rendered last, no heading)
|
||||
const groups = new Map();
|
||||
for (const gate of gates) {
|
||||
const icon = gate.gate_type === "car" ? "🚘" : "🚶";
|
||||
const label = gate.gate_type === "car" ? "Car" : "Pedestrian";
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `gate-btn ${gate.gate_type}`;
|
||||
btn.dataset.gateId = gate.id;
|
||||
btn.innerHTML = `<span class="icon">${icon}</span><span>${gate.name}</span>`;
|
||||
btn.addEventListener("click", () => handleOpenGate(btn, gate));
|
||||
grid.appendChild(btn);
|
||||
const key = gate.group_name || "";
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key).push(gate);
|
||||
}
|
||||
const hasNamedGroups = [...groups.keys()].some(k => k !== "");
|
||||
const sortedKeys = [...groups.keys()].sort((a, b) => {
|
||||
if (a === "" && b !== "") return 1;
|
||||
if (a !== "" && b === "") return -1;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const section = document.createElement("div");
|
||||
section.className = "gate-group";
|
||||
|
||||
if (hasNamedGroups && key) {
|
||||
const title = document.createElement("div");
|
||||
title.className = "gate-group-title";
|
||||
title.textContent = key;
|
||||
section.appendChild(title);
|
||||
}
|
||||
|
||||
const groupGrid = document.createElement("div");
|
||||
groupGrid.className = "gate-group-grid";
|
||||
|
||||
for (const gate of groups.get(key)) {
|
||||
const icon = gate.gate_type === "car" ? "🚘" : "🚶";
|
||||
const btn = document.createElement("button");
|
||||
btn.className = `gate-btn ${gate.gate_type}`;
|
||||
btn.dataset.gateId = gate.id;
|
||||
btn.innerHTML = `<span class="icon">${icon}</span><span>${gate.name}</span>`;
|
||||
btn.addEventListener("click", () => handleOpenGate(btn, gate));
|
||||
groupGrid.appendChild(btn);
|
||||
}
|
||||
|
||||
section.appendChild(groupGrid);
|
||||
grid.appendChild(section);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
@@ -64,11 +63,24 @@
|
||||
.app-header .sub { color: var(--text-muted); font-size: .8rem; }
|
||||
|
||||
#gates-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
padding: 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
.gate-group-title {
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: .6rem;
|
||||
}
|
||||
.gate-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
/* ── Loading state ───────────────────────────────────────────────────── */
|
||||
#loading-gates {
|
||||
@@ -86,8 +98,7 @@
|
||||
<div id="login-view">
|
||||
<img src="/static/logo.svg" alt="Lagomare" style="width:72px;height:72px;object-fit:contain;margin-bottom:.5rem" />
|
||||
<h1>Lagomare Gates</h1>
|
||||
<p class="subtitle">Enter your keypass to continue</p>
|
||||
<div class="card">
|
||||
<div class="card" style="margin-top:2rem">
|
||||
<form id="login-form">
|
||||
<div class="field">
|
||||
<label for="keypass-input">Keypass</label>
|
||||
@@ -113,12 +124,9 @@
|
||||
<!-- ── Gates view ──────────────────────────────────────────────────────── -->
|
||||
<div id="gates-view" class="hidden">
|
||||
<header class="app-header">
|
||||
<div style="display:flex;align-items:center;gap:.6rem">
|
||||
<img src="/static/logo.svg" alt="" style="width:32px;height:32px;object-fit:contain" />
|
||||
<div>
|
||||
<div class="app-header h2">Lagomare Gates</div>
|
||||
<div class="sub">Select a gate to open</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:.75rem">
|
||||
<img src="/static/logo.svg" alt="" style="width:50px;height:50px;object-fit:contain;flex-shrink:0" />
|
||||
<div class="app-header h2">Lagomare Gates</div>
|
||||
</div>
|
||||
<button id="logout-btn" class="btn btn-ghost" style="font-size:.85rem;padding:.5rem 1rem">
|
||||
Logout
|
||||
|
||||
Reference in New Issue
Block a user