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 .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)
|
||||
|
|
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>
|
||||
{% 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) %}
|
||||
<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">
|
||||
|
|
|
@ -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 @@
|
|||
<link rel="stylesheet" href="{{ url_for('static', filename='css/widget.css') }}">
|
||||
<style>
|
||||
{% block style %}
|
||||
|
||||
{% endblock %}
|
||||
</style>
|
||||
</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"
|
||||
integrity="sha384-tNYRX2RiDDDRKCJgPF8Pw3rTxC1GUe1pt5qH1SBmwcazrEUj7Ii4C1Tz9wCCRUI4"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="{{ url_for('static', filename='js/widget.js') }}"></script>
|
||||
<script>
|
||||
const searchInput = document.querySelector('#search');
|
||||
const searchButton = document.querySelector('#searchButton');
|
||||
|
|
|
@ -5,81 +5,67 @@
|
|||
{% block content %}
|
||||
|
||||
<form method="POST" action="{{ url_for('main.settings') }}">
|
||||
{{ breadcrumbs.breadcrumb_header(
|
||||
{{ breadcrumbs.breadcrumb_header(
|
||||
title=title,
|
||||
submit_button=True
|
||||
) }}
|
||||
) }}
|
||||
|
||||
<div class="container">
|
||||
<label for="brandInput" class="form-label">Brands</label>
|
||||
<div class="combo-box-widget">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control rounded-bottom-0" id="brandInput" name="brandInput" placeholder="Add a new brand">
|
||||
<button type="button" class="btn btn-primary rounded-bottom-0" id="addBrandButton" onclick="SettingsPage.addBrand();" disabled>
|
||||
{{ icons.plus(16) }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger rounded-bottom-0" id="removeBrandButton" onclick="SettingsPage.removeSelectedBrands()" disabled>
|
||||
{{ icons.minus(16) }}
|
||||
</button>
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
Inventory Settings
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ combos.render_combobox(
|
||||
id='brand',
|
||||
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>
|
||||
<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>
|
||||
</form>
|
||||
{% 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 %}
|
Loading…
Add table
Add a link
Reference in a new issue