Refactor worklog handling and rendering; enhance active worklog display and add settings page with brand management functionality

This commit is contained in:
Yaro Kasear 2025-06-18 09:33:33 -05:00
parent 3915b97231
commit e2b8579362
9 changed files with 145 additions and 42 deletions

View file

@ -61,7 +61,7 @@ worklog_headers = {
"Start Time": lambda i: {"text": i.start_time.strftime("%Y-%m-%d")}, "Start Time": lambda i: {"text": i.start_time.strftime("%Y-%m-%d")},
"End Time": lambda i: {"text": i.end_time.strftime("%Y-%m-%d")} if i.end_time else {"text": None}, "End Time": lambda i: {"text": i.end_time.strftime("%Y-%m-%d")} if i.end_time else {"text": None},
"Complete?": lambda i: {"text": i.complete, "type": "bool", "html": checked_box if i.complete else unchecked_box}, "Complete?": lambda i: {"text": i.complete, "type": "bool", "html": checked_box if i.complete else unchecked_box},
"Follow Up?": lambda i: {"text": i.followup, "type": "bool", "html": checked_box if i.followup else unchecked_box}, "Follow Up?": lambda i: {"text": i.followup, "type": "bool", "html": checked_box if i.followup else unchecked_box, "highlight": i.followup},
"Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box}, "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box},
} }
@ -136,13 +136,13 @@ def index():
worklog_query = eager_load_worklog_relationships( worklog_query = eager_load_worklog_relationships(
db.session.query(WorkLog) db.session.query(WorkLog)
).filter( ).filter(
(WorkLog.start_time < cutoff) & (WorkLog.complete == False) (WorkLog.complete == False)
) )
stale_worklogs = worklog_query.all() active_worklogs = worklog_query.all()
stale_count = len(stale_worklogs) active_count = len(active_worklogs)
stale_worklog_headers = { active_worklog_headers = {
k: v for k, v in worklog_headers.items() k: v for k, v in worklog_headers.items()
if k not in ['End Time', 'Quick Analysis?', 'Complete?', 'Follow Up?'] if k not in ['End Time', 'Quick Analysis?', 'Complete?', 'Follow Up?']
} }
@ -179,15 +179,28 @@ def index():
'name': 'Inventory Conditions' 'name': 'Inventory Conditions'
}] }]
active_worklog_rows = []
for log in active_worklogs:
# Create a dictionary of {column name: cell dict}
cells_by_key = {k: fn(log) for k, fn in worklog_headers.items()}
# Use original, full header set for logic
highlight = cells_by_key.get("Follow Up?", {}).get("highlight", False)
# Use only filtered headers — and in exact order
cells = [cells_by_key[k] for k in active_worklog_headers]
active_worklog_rows.append({
"id": log.id,
"cells": cells,
"highlight": highlight
})
return render_template( return render_template(
"index.html", "index.html",
title="Inventory Manager", active_count=active_count,
stale_count=stale_count, active_worklog_headers=active_worklog_headers,
stale_worklog_headers=stale_worklog_headers, active_worklog_rows=active_worklog_rows,
stale_worklog_rows=[{
"id": log.id,
"cells": [fn(log) for fn in stale_worklog_headers.values()]
} for log in stale_worklogs],
labels=labels, labels=labels,
datasets=datasets datasets=datasets
) )
@ -416,3 +429,8 @@ def search():
} }
return render_template('search.html', title=f"Database Search ({query})" if query else "Database Search", results=results, query=query) return render_template('search.html', title=f"Database Search ({query})" if query else "Database Search", results=results, query=query)
@main.route('/settings')
def settings():
brands = db.session.query(Brand).order_by(Brand.name).all()
return render_template('settings.html', title="Settings", brands=brands)

View file

@ -1,8 +0,0 @@
<!-- templates/worklog.html -->
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
{% endblock %}

View file

@ -1,3 +1,13 @@
{% macro gear(size=24) %}
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
class="bi bi-gear align-self-center" viewBox="0 0 16 16">
<path
d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0" />
<path
d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z" />
</svg>
{% endmacro %}
{% macro home(size=24) %} {% macro home(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-house align-self-center" viewBox="0 0 16 16"> class="bi bi-house align-self-center" viewBox="0 0 16 16">
@ -23,8 +33,8 @@
{% endmacro %} {% endmacro %}
{% macro link(size=24) %} {% macro link(size=24) %}
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor" class="bi bi-box-arrow-up-right align-self-center" <svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
viewBox="0 0 16 16"> class="bi bi-box-arrow-up-right align-self-center" viewBox="0 0 16 16">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5" /> d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5" />
<path fill-rule="evenodd" <path fill-rule="evenodd"
@ -50,6 +60,13 @@
</svg> </svg>
{% endmacro %} {% endmacro %}
{% macro minus(size=24) %}
<svg xmlns="http://www.w3.org/2000/svg" width="{{ size }}" height="{{ size }}" fill="currentColor"
class="bi bi-dash-lg align-self-center" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8" />
</svg>
{% endmacro %}
{% macro motherboard(size=24) %} {% macro motherboard(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-motherboard align-self-center" viewBox="0 0 16 16"> class="bi bi-motherboard align-self-center" viewBox="0 0 16 16">
@ -60,6 +77,14 @@
</svg> </svg>
{% endmacro %} {% 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">
<path fill-rule="evenodd"
d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2" />
</svg>
{% endmacro %}
{% macro search(size=24) %} {% macro search(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-search align-self-center" viewBox="0 0 16 16"> class="bi bi-search align-self-center" viewBox="0 0 16 16">

View file

@ -16,7 +16,7 @@
<tbody> <tbody>
{% for row in rows %} {% for row in rows %}
<tr {% if entry_route %}onclick="window.location='{{ url_for('main.' + entry_route, id=row.id) }}'" <tr {% if entry_route %}onclick="window.location='{{ url_for('main.' + entry_route, id=row.id) }}'"
style="cursor: pointer;" {% endif %}> style="cursor: pointer;"{% endif %}{% if row['highlight'] %} class="table-info"{% endif %}>
{% for cell in row.cells %} {% for cell in row.cells %}
<td class="text-nowrap{% if cell.type=='bool' %} text-center{% endif %}"> <td class="text-nowrap{% if cell.type=='bool' %} text-center{% endif %}">
{% if cell.type == 'bool' %} {% if cell.type == 'bool' %}

View file

@ -8,17 +8,16 @@
<h1 class="display-4">Welcome to Inventory Manager</h1> <h1 class="display-4">Welcome to Inventory Manager</h1>
<p class="lead">Find out about all of your assets.</p> <p class="lead">Find out about all of your assets.</p>
<div class="row"> <div class="row">
{% if stale_worklog_rows %} {% if active_worklog_rows %}
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Stale Worklogs</h5> <h5 class="card-title">Active Worklogs</h5>
<h6 class="card-subtitle mb-2 text-body-secondary">You have {{ stale_count }} worklogs <h6 class="card-subtitle mb-2 text-body-secondary">You have {{ active_count }} active worklogs.</h6>
that need attention!</h6>
{{ tables.render_table( {{ tables.render_table(
headers = stale_worklog_headers, headers = active_worklog_headers,
rows = stale_worklog_rows, rows = active_worklog_rows,
id = 'Stale Worklog', id = 'Active Worklog',
entry_route = 'worklog_entry', entry_route = 'worklog_entry',
per_page = 10 per_page = 10
)}} )}}

View file

@ -96,9 +96,10 @@ submit_button=True) }}
<div class="col-2"> <div class="col-2">
<label for="condition" class="form-label">Condition</label> <label for="condition" class="form-label">Condition</label>
<select name="condition" id="condition" class="form-select"> <select name="condition" id="condition" class="form-select">
<option>-</option>
{% for condition in ["Working", "Deployed", "Partially Inoperable", "Inoperable", "Unverified", {% for condition in ["Working", "Deployed", "Partially Inoperable", "Inoperable", "Unverified",
"Removed", "Disposed"] %} "Removed", "Disposed"] %}
<option value="{{ condition }}">{{ condition }}</option> <option value="{{ condition }}"{% if item.condition == condition %} selected{% endif %}>{{ condition }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View file

@ -10,21 +10,13 @@ title=title
<div class="container"> <div class="container">
{% if not category %} {% if not category %}
<div class="row">
<div class="col">
<h2 class="display-6 text-center">Find</h2>
</div>
</div>
<div class="row text-center">
{{ links.category_link(endpoint = 'search', label = 'Search', icon_html = icons.search(32)) }}
{{ links.category_link(endpoint = 'list_inventory', label = "List", icon_html = icons.table(32)) }}
</div>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h2 class="display-6 text-center mt-5">Browse</h2> <h2 class="display-6 text-center mt-5">Browse</h2>
</div> </div>
</div> </div>
<div class="row text-center"> <div class="row text-center">
{{ links.category_link(endpoint = 'list_inventory', label = "Full Listing", icon_html = icons.table(32)) }}
{{ links.category_link(endpoint = 'inventory_index', label = "By User", icon_html = icons.user(32), arguments = {'category': 'user'}) }} {{ links.category_link(endpoint = 'inventory_index', label = "By User", icon_html = icons.user(32), arguments = {'category': 'user'}) }}
{{ links.category_link(endpoint = 'inventory_index', label = 'By Location', icon_html = icons.map(32), arguments = {'category': 'location'}) }} {{ links.category_link(endpoint = 'inventory_index', label = 'By Location', icon_html = icons.map(32), arguments = {'category': 'location'}) }}
{{ links.category_link(endpoint = 'inventory_index', label = 'By Type', icon_html = icons.motherboard(32), arguments = {'category': 'type'}) }} {{ links.category_link(endpoint = 'inventory_index', label = 'By Type', icon_html = icons.motherboard(32), arguments = {'category': 'type'}) }}

View file

@ -10,7 +10,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Inventory{% endblock %}</title> <title>Inventory Manager{% if title %} - {% endif %}{% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous"> integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
<link <link
@ -55,6 +55,9 @@
<input type="text" class="form-control me-2" placeholder="Search" name="q" id="search" /> <input type="text" class="form-control me-2" placeholder="Search" name="q" id="search" />
<button class="btn btn-primary" type="submit" id="searchButton" disabled>Search</button> <button class="btn btn-primary" type="submit" id="searchButton" disabled>Search</button>
</form> </form>
<ul class="navbar-nav ms-2">
{{ links.navigation_link(endpoint='settings', label = '', icon_html = icons.gear()) }}
</ul>
</div> </div>
</div> </div>
</nav> </nav>
@ -66,7 +69,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/mark.min.js"
integrity="sha512-5CYOlHXGh6QpOFA/TeTylKLWfB3ftPsde7AnmhuitiTX4K5SqCLBeKro6sPS8ilsz1Q4NRx3v8Ko2IBiszzdww==" integrity="sha512-5CYOlHXGh6QpOFA/TeTylKLWfB3ftPsde7AnmhuitiTX4K5SqCLBeKro6sPS8ilsz1Q4NRx3v8Ko2IBiszzdww=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script> <script src="https://cdn.plot.ly/plotly-3.0.1.min.js" charset="utf-8"></script>
<script <script
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"

73
templates/settings.html Normal file
View file

@ -0,0 +1,73 @@
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<form method="POST" action="{{ url_for('main.settings') }}">
{{ breadcrumbs.breadcrumb_header(
title=title,
submit_button=True
) }}
<div class="container">
<label for="brandInput" class="form-label">Brands</label>
<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="addBrand();" disabled>
{{ icons.plus(16) }}
</button>
<button type="button" class="btn btn-danger rounded-bottom-0" id="removeBrandButton" onclick="removeSelectedBrands()" disabled>
{{ icons.minus(16) }}
</button>
</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>
</form>
{% endblock %}
{% block script %}
const brandInput = document.querySelector('#brandInput');
const brandList = document.querySelector('#brandList');
const addBrandButton = document.querySelector('#addBrandButton');
const removeBrandButton = document.querySelector('#removeBrandButton');
brandInput.addEventListener('input', () => {
addBrandButton.disabled = brandInput.value.trim() === '';
});
brandList.addEventListener('change', () => {
removeBrandButton.disabled = brandList.selectedOptions.length === 0;
});
function sortOptions() {
const options = Array.from(brandList.options);
options.sort((a, b) => a.text.localeCompare(b.text));
brandList.innerHTML = '';
options.forEach(option => brandList.appendChild(option));
}
let tempIdCounter = -1;
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;
}
{% endblock %}