Enhance settings page; integrate combo box widgets for brands, types, sections, and functions, and add pencil icon for editing functionality
This commit is contained in:
parent
dba2438937
commit
ad413c3f1b
6 changed files with 177 additions and 67 deletions
|
@ -1,5 +1,5 @@
|
||||||
from flask import Blueprint, render_template, url_for, request, redirect
|
from flask import Blueprint, render_template, url_for, request, redirect
|
||||||
from .models import Brand, Item, Inventory, RoomFunction, User, WorkLog, Room
|
from .models import Brand, Item, Inventory, RoomFunction, User, WorkLog, Room, Area
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
from typing import Callable, Any, List
|
from typing import Callable, Any, List
|
||||||
|
@ -433,4 +433,7 @@ def search():
|
||||||
@main.route('/settings')
|
@main.route('/settings')
|
||||||
def settings():
|
def settings():
|
||||||
brands = db.session.query(Brand).order_by(Brand.name).all()
|
brands = db.session.query(Brand).order_by(Brand.name).all()
|
||||||
return render_template('settings.html', title="Settings", brands=brands)
|
types = db.session.query(Item.id, Item.description.label("name")).order_by(Item.description).all()
|
||||||
|
sections = db.session.query(Area).order_by(Area.name).all()
|
||||||
|
functions = db.session.query(RoomFunction.id, RoomFunction.description.label("name")).order_by(RoomFunction.description).all()
|
||||||
|
return render_template('settings.html', title="Settings", brands=brands, types=types, sections=sections, functions=functions)
|
||||||
|
|
81
static/js/widget.js
Normal file
81
static/js/widget.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
const ComboBoxWidget = (() => {
|
||||||
|
let tempIdCounter = -1;
|
||||||
|
|
||||||
|
function initComboBox(ns) {
|
||||||
|
const input = document.querySelector(`#${ns}-input`);
|
||||||
|
const list = document.querySelector(`#${ns}-list`);
|
||||||
|
const addBtn = document.querySelector(`#${ns}-add`);
|
||||||
|
const removeBtn = document.querySelector(`#${ns}-remove`);
|
||||||
|
let currentlyEditing = null;
|
||||||
|
|
||||||
|
if (!input || !list || !addBtn || !removeBtn) {
|
||||||
|
console.warn(`ComboBoxWidget: Missing elements for namespace '${ns}'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAddButtonIcon() {
|
||||||
|
addBtn.innerHTML = currentlyEditing ? icons.edit : icons.add;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
addBtn.disabled = input.value.trim() === '';
|
||||||
|
});
|
||||||
|
|
||||||
|
list.addEventListener('change', () => {
|
||||||
|
const selected = list.selectedOptions;
|
||||||
|
removeBtn.disabled = selected.length === 0;
|
||||||
|
|
||||||
|
if (selected.length === 1) {
|
||||||
|
// Load the text into input for editing
|
||||||
|
input.value = selected[0].textContent;
|
||||||
|
addBtn.disabled = input.value.trim() === '';
|
||||||
|
currentlyEditing = selected[0];
|
||||||
|
} else {
|
||||||
|
input.value = '';
|
||||||
|
currentlyEditing = null;
|
||||||
|
addBtn.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAddButtonIcon();
|
||||||
|
});
|
||||||
|
|
||||||
|
addBtn.addEventListener('click', () => {
|
||||||
|
const newItem = input.value.trim();
|
||||||
|
if (!newItem) return;
|
||||||
|
|
||||||
|
if (currentlyEditing) {
|
||||||
|
currentlyEditing.textContent = newItem;
|
||||||
|
currentlyEditing = null;
|
||||||
|
} else {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.textContent = newItem;
|
||||||
|
option.value = tempIdCounter--;
|
||||||
|
list.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
addBtn.disabled = true;
|
||||||
|
removeBtn.disabled = true;
|
||||||
|
sortOptions(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeBtn.addEventListener('click', () => {
|
||||||
|
Array.from(list.selectedOptions).forEach(opt => opt.remove());
|
||||||
|
currentlyEditing = null;
|
||||||
|
removeBtn.disabled = true;
|
||||||
|
input.value = '';
|
||||||
|
addBtn.disabled = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortOptions(selectElement) {
|
||||||
|
const sorted = Array.from(selectElement.options)
|
||||||
|
.sort((a, b) => a.text.localeCompare(b.text));
|
||||||
|
selectElement.innerHTML = '';
|
||||||
|
sorted.forEach(option => selectElement.appendChild(option));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initComboBox
|
||||||
|
};
|
||||||
|
})();
|
29
templates/fragments/_combobox_fragment.html
Normal file
29
templates/fragments/_combobox_fragment.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% import "fragments/_icon_fragment.html" as icons %}
|
||||||
|
|
||||||
|
{% macro render_combobox(id, options, label=none, placeholder=none) %}
|
||||||
|
{% if label %}
|
||||||
|
<label for="{{ id }}-input" class="form-label">{{ label }}</label>
|
||||||
|
{% endif %}
|
||||||
|
<div class="combo-box-widget" id="{{ id }}-container">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control rounded-bottom-0" id="{{ id }}-input"{% if placeholder %} placeholder="{{ placeholder }}"{% endif %}>
|
||||||
|
<button type="button" class="btn btn-primary rounded-bottom-0" id="{{ id }}-add" disabled>
|
||||||
|
{{ icons.plus(16) }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-danger rounded-bottom-0" id="{{ id }}-remove" disabled>
|
||||||
|
{{ icons.minus(16) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select class="form-select border-top-0 rounded-top-0" id="{{ id }}-list" name="{{ id }}" size="10" multiple>
|
||||||
|
{% for option in options %}
|
||||||
|
<option value="{{ option.id }}">{{ option.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
ComboBoxWidget.initComboBox("{{ id }}");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endmacro %}
|
|
@ -77,6 +77,14 @@
|
||||||
</svg>
|
</svg>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro pencil(size=24) %}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
|
||||||
|
class="bi bi-pencil align-self-center" viewBox="0 0 16 16">
|
||||||
|
<path
|
||||||
|
d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325" />
|
||||||
|
</svg>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro plus(size=24) %}
|
{% macro plus(size=24) %}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
|
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
|
||||||
class="bi bi-plus-lg align-self-center" viewBox="0 0 16 16">
|
class="bi bi-plus-lg align-self-center" viewBox="0 0 16 16">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% import "fragments/_breadcrumb_fragment.html" as breadcrumbs %}
|
{% import "fragments/_breadcrumb_fragment.html" as breadcrumbs %}
|
||||||
|
{% import "fragments/_combobox_fragment.html" as combos %}
|
||||||
{% import "fragments/_icon_fragment.html" as icons %}
|
{% import "fragments/_icon_fragment.html" as icons %}
|
||||||
{% import "fragments/_link_fragment.html" as links %}
|
{% import "fragments/_link_fragment.html" as links %}
|
||||||
{% import "fragments/_table_fragment.html" as tables %}
|
{% import "fragments/_table_fragment.html" as tables %}
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/widget.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/widget.css') }}">
|
||||||
<style>
|
<style>
|
||||||
{% block style %}
|
{% block style %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
@ -62,6 +64,7 @@
|
||||||
src="https://cdn.datatables.net/v/bs5/jq-3.7.0/moment-2.29.4/dt-2.3.2/b-3.2.3/b-colvis-3.2.3/b-print-3.2.3/cr-2.1.1/cc-1.0.4/r-3.0.4/rg-1.5.1/rr-1.5.0/sc-2.4.3/sr-1.4.1/datatables.min.js"
|
src="https://cdn.datatables.net/v/bs5/jq-3.7.0/moment-2.29.4/dt-2.3.2/b-3.2.3/b-colvis-3.2.3/b-print-3.2.3/cr-2.1.1/cc-1.0.4/r-3.0.4/rg-1.5.1/rr-1.5.0/sc-2.4.3/sr-1.4.1/datatables.min.js"
|
||||||
integrity="sha384-tNYRX2RiDDDRKCJgPF8Pw3rTxC1GUe1pt5qH1SBmwcazrEUj7Ii4C1Tz9wCCRUI4"
|
integrity="sha384-tNYRX2RiDDDRKCJgPF8Pw3rTxC1GUe1pt5qH1SBmwcazrEUj7Ii4C1Tz9wCCRUI4"
|
||||||
crossorigin="anonymous"></script>
|
crossorigin="anonymous"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/widget.js') }}"></script>
|
||||||
<script>
|
<script>
|
||||||
const searchInput = document.querySelector('#search');
|
const searchInput = document.querySelector('#search');
|
||||||
const searchButton = document.querySelector('#searchButton');
|
const searchButton = document.querySelector('#searchButton');
|
||||||
|
|
|
@ -11,75 +11,61 @@
|
||||||
) }}
|
) }}
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<label for="brandInput" class="form-label">Brands</label>
|
<div class="card mb-3">
|
||||||
<div class="combo-box-widget">
|
<div class="card-body">
|
||||||
<div class="input-group">
|
<h5 class="card-title">
|
||||||
<input type="text" class="form-control rounded-bottom-0" id="brandInput" name="brandInput" placeholder="Add a new brand">
|
Inventory Settings
|
||||||
<button type="button" class="btn btn-primary rounded-bottom-0" id="addBrandButton" onclick="SettingsPage.addBrand();" disabled>
|
</h5>
|
||||||
{{ icons.plus(16) }}
|
<div class="row">
|
||||||
</button>
|
<div class="col">
|
||||||
<button type="button" class="btn btn-danger rounded-bottom-0" id="removeBrandButton" onclick="SettingsPage.removeSelectedBrands()" disabled>
|
{{ combos.render_combobox(
|
||||||
{{ icons.minus(16) }}
|
id='brand',
|
||||||
</button>
|
options=brands,
|
||||||
|
label='Brands',
|
||||||
|
placeholder='Add a new brand'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{{ combos.render_combobox(
|
||||||
|
id='type',
|
||||||
|
options=types,
|
||||||
|
label='Inventory Types',
|
||||||
|
placeholder='Add a new type'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Location Settings</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
{{ combos.render_combobox(
|
||||||
|
id='section',
|
||||||
|
options=sections,
|
||||||
|
label='Sections',
|
||||||
|
placeholder='Add a new section'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{{ combos.render_combobox(
|
||||||
|
id='function',
|
||||||
|
options=functions,
|
||||||
|
label='Functions',
|
||||||
|
placeholder='Add a new function'
|
||||||
|
) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select class="form-select border-top-0 rounded-top-0" id="brandList" size="10" multiple>
|
|
||||||
{% for brand in brands %}
|
|
||||||
<option value="{{ brand.id }}">{{ brand.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
const SettingsPage = (() => {
|
const icons = {
|
||||||
const brandInput = document.querySelector('#brandInput');
|
add: `{{ icons.plus(16)|safe }}`,
|
||||||
const brandList = document.querySelector('#brandList');
|
edit: `{{ icons.pencil(16)|safe }}`,
|
||||||
const addBrandButton = document.querySelector('#addBrandButton');
|
|
||||||
const removeBrandButton = document.querySelector('#removeBrandButton');
|
|
||||||
let tempIdCounter = -1;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
brandInput.addEventListener('input', () => {
|
|
||||||
addBrandButton.disabled = brandInput.value.trim() === '';
|
|
||||||
});
|
|
||||||
|
|
||||||
brandList.addEventListener('change', () => {
|
|
||||||
removeBrandButton.disabled = brandList.selectedOptions.length === 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBrand() {
|
|
||||||
const newBrand = brandInput.value.trim();
|
|
||||||
if (newBrand) {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.textContent = newBrand;
|
|
||||||
option.value = tempIdCounter--;
|
|
||||||
brandList.appendChild(option);
|
|
||||||
brandInput.value = '';
|
|
||||||
addBrandButton.disabled = true;
|
|
||||||
sortOptions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSelectedBrands() {
|
|
||||||
Array.from(brandList.selectedOptions).forEach(option => option.remove());
|
|
||||||
removeBrandButton.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortOptions() {
|
|
||||||
Array.from(brandList.options)
|
|
||||||
.sort((a, b) => a.text.localeCompare(b.text))
|
|
||||||
.forEach(option => brandList.appendChild(option));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
init,
|
|
||||||
addBrand,
|
|
||||||
removeSelectedBrands
|
|
||||||
};
|
};
|
||||||
})();
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', SettingsPage.init);
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue