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:
Yaro Kasear 2025-06-18 14:13:16 -05:00
parent dba2438937
commit ad413c3f1b
6 changed files with 177 additions and 67 deletions

View file

@ -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
View 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
};
})();

View 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 %}

View file

@ -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">

View file

@ -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');

View file

@ -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 %}