Files
location_managemet/app/templates/boards/edit_entities.html
2026-02-26 19:24:17 +02:00

319 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Configure Entities {{ board.name }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('boards.list_boards') }}">Boards</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('boards.board_detail', board_id=board.id) }}">{{ board.name }}</a></li>
<li class="breadcrumb-item active">Configure Entities</li>
</ol>
</nav>
<h2 class="fw-bold mb-1">Configure Entities</h2>
<p class="text-secondary mb-4">
Set a type, custom name, and optional icon override for each relay and input.
The type determines the icon, status colors, and state labels shown everywhere.
</p>
<form method="POST" action="{{ url_for('boards.edit_entities', board_id=board.id) }}">
<!-- ── Relay entities ────────────────────────────────────────────────────── -->
<h5 class="fw-semibold mb-3"><i class="bi bi-lightning-charge text-warning me-1"></i> Relay Outputs</h5>
<div class="row g-3 mb-4">
{% for n in range(1, board.num_relays + 1) %}
{% set e = board.get_relay_entity(n) %}
<div class="col-md-6">
<div class="card border-0 rounded-4 entity-card" data-entity="relay_{{ n }}" data-kind="relay">
<div class="card-body">
<!-- Header: icon preview + number label -->
<div class="d-flex align-items-center gap-3 mb-3">
<div class="entity-icon-wrap rounded-3 d-flex align-items-center justify-content-center"
style="width:56px;height:56px;background:var(--bs-secondary-bg)">
<i class="bi {{ e.icon }} entity-icon-preview" style="font-size:1.8rem" id="icon-preview-relay-{{ n }}"></i>
</div>
<div class="flex-grow-1">
<div class="text-secondary small mb-1">Relay {{ n }}</div>
<input type="text" class="form-control form-control-sm"
name="relay_{{ n }}_name"
value="{{ board.entities.get('relay_' ~ n, {}).get('name', '') }}"
maxlength="20" placeholder="{{ e.name }}" />
<div class="form-text" style="font-size:.7rem">Leave blank to use type default</div>
</div>
</div>
<!-- Type selector -->
<div class="mb-3">
<div class="small text-secondary mb-2">Entity type</div>
<div class="d-flex flex-wrap gap-2">
{% for tkey, tdef in relay_types.items() %}
<label class="entity-type-pill" title="{{ tdef.label }}"
data-icon="{{ tdef.icon }}"
data-target="relay_{{ n }}">
<input type="radio" name="relay_{{ n }}_type" value="{{ tkey }}" hidden
{% if e.type == tkey %}checked{% endif %} />
<i class="bi {{ tdef.icon }}"></i>
<span>{{ tdef.label }}</span>
</label>
{% endfor %}
</div>
</div>
<!-- Custom icon override -->
<details class="mt-2">
<summary class="text-secondary small" style="cursor:pointer">
<i class="bi bi-palette me-1"></i> Custom icon override
</summary>
<div class="mt-2">
<div class="input-group input-group-sm mb-2">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control icon-text-input"
name="relay_{{ n }}_icon"
value="{{ board.entities.get('relay_' ~ n, {}).get('icon', '') }}"
placeholder="e.g. bi-lightbulb-fill"
data-target="relay_{{ n }}" />
<button type="button" class="btn btn-outline-secondary btn-sm icon-clear-btn"
data-target="relay_{{ n }}">
<i class="bi bi-x"></i>
</button>
</div>
<div class="icon-palette d-flex flex-wrap gap-1">
{% for icon_cls, icon_name in icon_palette %}
<button type="button" class="btn btn-sm btn-outline-secondary icon-pick-btn p-1"
title="{{ icon_name }}" data-icon="{{ icon_cls }}" data-target="relay_{{ n }}">
<i class="bi {{ icon_cls }}" style="font-size:1.1rem"></i>
</button>
{% endfor %}
</div>
</div>
</details>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- ── Input entities ────────────────────────────────────────────────────── -->
<h5 class="fw-semibold mb-3"><i class="bi bi-activity text-info me-1"></i> Digital Inputs</h5>
<div class="row g-3 mb-4">
{% for n in range(1, board.num_inputs + 1) %}
{% set e = board.get_input_entity(n) %}
<div class="col-md-6">
<div class="card border-0 rounded-4 entity-card" data-entity="input_{{ n }}" data-kind="input">
<div class="card-body">
<!-- Header: icon preview + number label -->
<div class="d-flex align-items-center gap-3 mb-3">
<div class="entity-icon-wrap rounded-3 d-flex align-items-center justify-content-center"
style="width:56px;height:56px;background:var(--bs-secondary-bg)">
<i class="bi {{ e.icon }} entity-icon-preview" style="font-size:1.8rem" id="icon-preview-input-{{ n }}"></i>
</div>
<div class="flex-grow-1">
<div class="text-secondary small mb-1">Input {{ n }}</div>
<input type="text" class="form-control form-control-sm"
name="input_{{ n }}_name"
value="{{ board.entities.get('input_' ~ n, {}).get('name', '') }}"
maxlength="20" placeholder="{{ e.name }}" />
<div class="form-text" style="font-size:.7rem">Leave blank to use type default</div>
</div>
</div>
<!-- Type selector -->
<div class="mb-3">
<div class="small text-secondary mb-2">Entity type</div>
<div class="d-flex flex-wrap gap-2">
{% for tkey, tdef in input_types.items() %}
<label class="entity-type-pill" title="{{ tdef.label }}"
data-icon="{{ tdef.icon }}"
data-target="input_{{ n }}">
<input type="radio" name="input_{{ n }}_type" value="{{ tkey }}" hidden
{% if e.type == tkey %}checked{% endif %} />
<i class="bi {{ tdef.icon }}"></i>
<span>{{ tdef.label }}</span>
</label>
{% endfor %}
</div>
</div>
<!-- Custom icon override -->
<details class="mt-2">
<summary class="text-secondary small" style="cursor:pointer">
<i class="bi bi-palette me-1"></i> Custom icon override
</summary>
<div class="mt-2">
<div class="input-group input-group-sm mb-2">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control icon-text-input"
name="input_{{ n }}_icon"
value="{{ board.entities.get('input_' ~ n, {}).get('icon', '') }}"
placeholder="e.g. bi-door-open-fill"
data-target="input_{{ n }}" />
<button type="button" class="btn btn-outline-secondary btn-sm icon-clear-btn"
data-target="input_{{ n }}">
<i class="bi bi-x"></i>
</button>
</div>
<div class="icon-palette d-flex flex-wrap gap-1">
{% for icon_cls, icon_name in icon_palette %}
<button type="button" class="btn btn-sm btn-outline-secondary icon-pick-btn p-1"
title="{{ icon_name }}" data-icon="{{ icon_cls }}" data-target="input_{{ n }}">
<i class="bi {{ icon_cls }}" style="font-size:1.1rem"></i>
</button>
{% endfor %}
</div>
</div>
</details>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- ── Actions ────────────────────────────────────────────────────────────── -->
<div class="d-flex gap-2 mt-2 mb-5">
<button type="submit" class="btn btn-primary">
<i class="bi bi-floppy me-1"></i> Save Configuration
</button>
<a href="{{ url_for('boards.board_detail', board_id=board.id) }}" class="btn btn-outline-secondary">
Cancel
</a>
</div>
</form>
{% endblock %}
{% block scripts %}
<style>
/* ── Entity type pills ───────────────────────────────────────────── */
.entity-type-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 10px;
border-radius: 20px;
border: 1px solid var(--bs-border-color);
cursor: pointer;
font-size: .8rem;
transition: background .15s, border-color .15s, color .15s;
user-select: none;
}
.entity-type-pill:hover {
border-color: var(--bs-primary);
color: var(--bs-primary);
}
.entity-type-pill:has(input:checked) {
border-color: var(--bs-primary);
background: var(--bs-primary-bg-subtle, rgba(13,110,253,.15));
color: var(--bs-primary);
font-weight: 600;
}
/* ── Icon palette grid ───────────────────────────────────────────── */
.icon-palette {
max-height: 160px;
overflow-y: auto;
padding: 4px;
background: var(--bs-secondary-bg);
border-radius: .5rem;
}
.icon-pick-btn {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-pick-btn.active {
background: var(--bs-primary);
border-color: var(--bs-primary);
color: #fff;
}
</style>
<script>
/* ── Live icon preview ───────────────────────────────────────────────────── */
function resolvePreview(target) {
const iconInput = document.querySelector(`.icon-text-input[data-target="${target}"]`);
const customIcon = iconInput ? iconInput.value.trim() : "";
const preview = document.getElementById(`icon-preview-${target.replace("_", "-")}`);
if (!preview) return;
if (customIcon) {
// Remove all bi-* classes, add the custom one
preview.className = preview.className.replace(/\bbi-\S+/g, "").trim();
const cls = customIcon.startsWith("bi-") ? customIcon : `bi-${customIcon}`;
preview.classList.add(cls);
} else {
// Use the checked type's icon
const checkedType = document.querySelector(`input[name="${target}_type"]:checked`);
if (checkedType) {
const pill = checkedType.closest(".entity-type-pill");
const typeIcon = pill ? pill.getAttribute("data-icon") : null;
if (typeIcon) {
preview.className = preview.className.replace(/\bbi-\S+/g, "").trim();
preview.classList.add(typeIcon);
}
}
}
}
/* ── Type pill selection ─────────────────────────────────────────────────── */
document.querySelectorAll(".entity-type-pill").forEach(pill => {
pill.addEventListener("click", () => {
const target = pill.getAttribute("data-target");
// Small delay so the radio value has flipped
requestAnimationFrame(() => resolvePreview(target));
});
});
/* ── Custom icon text input ──────────────────────────────────────────────── */
document.querySelectorAll(".icon-text-input").forEach(inp => {
inp.addEventListener("input", () => resolvePreview(inp.getAttribute("data-target")));
});
/* ── Icon palette pick ───────────────────────────────────────────────────── */
document.querySelectorAll(".icon-pick-btn").forEach(btn => {
btn.addEventListener("click", () => {
const icon = btn.getAttribute("data-icon");
const target = btn.getAttribute("data-target");
const inp = document.querySelector(`.icon-text-input[data-target="${target}"]`);
if (inp) inp.value = icon;
// Highlight selected palette button
btn.closest(".icon-palette").querySelectorAll(".icon-pick-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
resolvePreview(target);
});
});
/* ── Clear custom icon ───────────────────────────────────────────────────── */
document.querySelectorAll(".icon-clear-btn").forEach(btn => {
btn.addEventListener("click", () => {
const target = btn.getAttribute("data-target");
const inp = document.querySelector(`.icon-text-input[data-target="${target}"]`);
if (inp) inp.value = "";
// Deselect all palette buttons for this target
const card = btn.closest(".card-body");
if (card) card.querySelectorAll(".icon-pick-btn").forEach(b => b.classList.remove("active"));
resolvePreview(target);
});
});
/* ── Init: highlight pre-selected palette icons, run initial previews ────── */
document.querySelectorAll(".icon-text-input").forEach(inp => {
const val = inp.value.trim();
const target = inp.getAttribute("data-target");
if (val) {
const match = inp.closest(".card-body").querySelector(`.icon-pick-btn[data-icon="${val}"]`);
if (match) match.classList.add("active");
}
resolvePreview(target);
});
</script>
{% endblock %}