From ad413c3f1b603d36047fbc307574f699be8e1331 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Wed, 18 Jun 2025 14:13:16 -0500 Subject: [PATCH] Enhance settings page; integrate combo box widgets for brands, types, sections, and functions, and add pencil icon for editing functionality --- routes.py | 7 +- static/js/widget.js | 81 ++++++++++++++ templates/fragments/_combobox_fragment.html | 29 +++++ templates/fragments/_icon_fragment.html | 8 ++ templates/layout.html | 3 + templates/settings.html | 116 +++++++++----------- 6 files changed, 177 insertions(+), 67 deletions(-) create mode 100644 static/js/widget.js create mode 100644 templates/fragments/_combobox_fragment.html diff --git a/routes.py b/routes.py index b39e427..b8e6934 100644 --- a/routes.py +++ b/routes.py @@ -1,5 +1,5 @@ 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.orm import aliased from typing import Callable, Any, List @@ -433,4 +433,7 @@ def search(): @main.route('/settings') def settings(): 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) diff --git a/static/js/widget.js b/static/js/widget.js new file mode 100644 index 0000000..fd571a4 --- /dev/null +++ b/static/js/widget.js @@ -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 + }; +})(); diff --git a/templates/fragments/_combobox_fragment.html b/templates/fragments/_combobox_fragment.html new file mode 100644 index 0000000..e5919ae --- /dev/null +++ b/templates/fragments/_combobox_fragment.html @@ -0,0 +1,29 @@ +{% import "fragments/_icon_fragment.html" as icons %} + +{% macro render_combobox(id, options, label=none, placeholder=none) %} + {% if label %} + + {% endif %} +
+
+ + + +
+ +
+ + +{% endmacro %} \ No newline at end of file diff --git a/templates/fragments/_icon_fragment.html b/templates/fragments/_icon_fragment.html index 1258994..ba8bc24 100644 --- a/templates/fragments/_icon_fragment.html +++ b/templates/fragments/_icon_fragment.html @@ -77,6 +77,14 @@ {% endmacro %} +{% macro pencil(size=24) %} + + + +{% endmacro %} + {% macro plus(size=24) %} diff --git a/templates/layout.html b/templates/layout.html index fa3f463..e6136a5 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,4 +1,5 @@ {% import "fragments/_breadcrumb_fragment.html" as breadcrumbs %} +{% import "fragments/_combobox_fragment.html" as combos %} {% import "fragments/_icon_fragment.html" as icons %} {% import "fragments/_link_fragment.html" as links %} {% import "fragments/_table_fragment.html" as tables %} @@ -20,6 +21,7 @@ @@ -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" integrity="sha384-tNYRX2RiDDDRKCJgPF8Pw3rTxC1GUe1pt5qH1SBmwcazrEUj7Ii4C1Tz9wCCRUI4" crossorigin="anonymous"> +
- -
-
- - - +
+
+
+ Inventory Settings +
+
+
+ {{ combos.render_combobox( + id='brand', + options=brands, + label='Brands', + placeholder='Add a new brand' + ) }} +
+
+ {{ combos.render_combobox( + id='type', + options=types, + label='Inventory Types', + placeholder='Add a new type' + ) }} +
+
+
+
+
+
+
Location Settings
+
+
+ {{ combos.render_combobox( + id='section', + options=sections, + label='Sections', + placeholder='Add a new section' + ) }} +
+
+ {{ combos.render_combobox( + id='function', + options=functions, + label='Functions', + placeholder='Add a new function' + ) }} +
+
-
{% endblock %} {% block script %} -const SettingsPage = (() => { - const brandInput = document.querySelector('#brandInput'); - const brandList = document.querySelector('#brandList'); - 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 + const icons = { + add: `{{ icons.plus(16)|safe }}`, + edit: `{{ icons.pencil(16)|safe }}`, }; -})(); - -document.addEventListener('DOMContentLoaded', SettingsPage.init); {% endblock %} \ No newline at end of file