Enhance table rendering functionality: refactor headers and rows handling, add dynamic table support, and implement refresh capabilities. Update related templates and JavaScript for improved data management.
This commit is contained in:
parent
1e05ad16ce
commit
79d94ff950
13 changed files with 132 additions and 43 deletions
|
|
@ -36,18 +36,18 @@ ROUTE_BREADCRUMBS = {
|
|||
}
|
||||
|
||||
inventory_headers = {
|
||||
"Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
|
||||
"Identifier": lambda i: {"text": i.identifier},
|
||||
"Name": lambda i: {"text": i.name},
|
||||
"Serial Number": lambda i: {"text": i.serial},
|
||||
"Bar Code": lambda i: {"text": i.barcode},
|
||||
"Brand": lambda i: {"text": i.brand.name} if i.brand else {"text": None},
|
||||
"Model": lambda i: {"text": i.model},
|
||||
"Item Type": lambda i: {"text": i.device_type.description} if i.device_type else {"text": None},
|
||||
"Shared?": lambda i: {"text": i.shared, "type": "bool", "html": checked_box if i.shared else unchecked_box},
|
||||
"Owner": lambda i: {"text": i.owner.identifier, "url": url_for("main.user", id=i.owner.id)} if i.owner else {"text": None},
|
||||
"Location": lambda i: {"text": i.location.identifier} if i.location else {"Text": None},
|
||||
"Condition": lambda i: {"text": i.condition}
|
||||
"Date Entered": lambda i: {"field": "timestamp", "text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
|
||||
"Identifier": lambda i: {"field": "identifier", "text": i.identifier},
|
||||
"Name": lambda i: {"field": "name", "text": i.name},
|
||||
"Serial Number": lambda i: {"field": "serial", "text": i.serial},
|
||||
"Bar Code": lambda i: {"field": "barcode", "text": i.barcode},
|
||||
"Brand": lambda i: {"field": "brand.name", "text": i.brand.name} if i.brand else {"text": None},
|
||||
"Model": lambda i: {"field": "model", "text": i.model},
|
||||
"Item Type": lambda i: {"field": "device_type.description", "text": i.device_type.description} if i.device_type else {"text": None},
|
||||
"Shared?": lambda i: {"field": "shared", "text": i.shared, "type": "bool", "html": checked_box if i.shared else unchecked_box},
|
||||
"Owner": lambda i: {"field": "owner.identifier", "text": i.owner.identifier, "url": url_for("main.user", id=i.owner.id)} if i.owner else {"text": None},
|
||||
"Location": lambda i: {"field": "location.identifier", "text": i.location.identifier} if i.location else {"Text": None},
|
||||
"Condition": lambda i: {"field": "condition", "text": i.condition}
|
||||
}
|
||||
|
||||
checked_box = '''
|
||||
|
|
@ -75,17 +75,17 @@ FILTER_MAP = {
|
|||
}
|
||||
|
||||
user_headers = {
|
||||
"Last Name": lambda i: {"text": i.last_name},
|
||||
"First Name": lambda i: {"text": i.first_name},
|
||||
"Title": lambda i: {"text": i.title},
|
||||
"Supervisor": lambda i: {"text": i.supervisor.identifier, "url": url_for("main.user", id=i.supervisor.id)} if i.supervisor else {"text": None},
|
||||
"Location": lambda i: {"text": i.location.identifier} if i.location else {"text": None},
|
||||
"Staff?": lambda i: {"text": i.staff, "type": "bool", "html": checked_box if i.staff else unchecked_box},
|
||||
"Active?": lambda i: {"text": i.active, "type": "bool", "html": checked_box if i.active else unchecked_box}
|
||||
"Last Name": lambda i: {"field": "last_name","text": i.last_name},
|
||||
"First Name": lambda i: {"field": "first_name","text": i.first_name},
|
||||
"Title": lambda i: {"field": "title","text": i.title},
|
||||
"Supervisor": lambda i: {"field": "supervisor,identifier","text": i.supervisor.identifier, "url": url_for("main.user", id=i.supervisor.id)} if i.supervisor else {"text": None},
|
||||
"Location": lambda i: {"field": "location,identifier","text": i.location.identifier} if i.location else {"text": None},
|
||||
"Staff?": lambda i: {"field": "staff","text": i.staff, "type": "bool", "html": checked_box if i.staff else unchecked_box},
|
||||
"Active?": lambda i: {"field": "active","text": i.active, "type": "bool", "html": checked_box if i.active else unchecked_box}
|
||||
}
|
||||
|
||||
worklog_headers = {
|
||||
"Contact": lambda i: {"text": i.contact.identifier, "url": url_for("main.user", id=i.contact.id)} if i.contact else {"Text": None},
|
||||
"Contact": lambda i: {"text": i.contact.identifier, "url": url_for("main.user_item", id=i.contact.id)} if i.contact else {"Text": None},
|
||||
"Work Item": lambda i: {"text": i.work_item.identifier, "url": url_for('main.inventory_item',id=i.work_item.id)} if i.work_item else {"text": None},
|
||||
"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},
|
||||
|
|
|
|||
|
|
@ -43,13 +43,18 @@ def list_inventory():
|
|||
inventory = query.all()
|
||||
inventory = sorted(inventory, key=lambda i: i.identifier)
|
||||
|
||||
rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory]
|
||||
fields = [d['field'] for d in rows[0]['cells']]
|
||||
|
||||
return render_template(
|
||||
'table.html',
|
||||
title=f"Inventory Listing ({filter_name})" if filter_by else "Inventory Listing",
|
||||
header=inventory_headers,
|
||||
rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory],
|
||||
fields=fields,
|
||||
rows=rows,
|
||||
entry_route = 'inventory_item',
|
||||
csv_route = 'inventory'
|
||||
csv_route = 'inventory',
|
||||
model_name = 'inventory'
|
||||
)
|
||||
|
||||
@main.route("/inventory/index")
|
||||
|
|
|
|||
|
|
@ -17,18 +17,19 @@ def list_users():
|
|||
return render_template(
|
||||
'table.html',
|
||||
header = user_headers,
|
||||
rows = [{"id": user.id, "cells": [fn(user) for fn in user_headers.values()]} for user in users],
|
||||
model_name = 'user',
|
||||
title = "Users",
|
||||
entry_route = 'user',
|
||||
csv_route = 'user'
|
||||
entry_route = 'user_item',
|
||||
csv_route = 'user',
|
||||
fields = ['last_name', 'first_name', 'title', 'supervisor.identifier', 'location.identifier', 'staff', 'active'],
|
||||
)
|
||||
|
||||
@main.route("/user/<id>")
|
||||
def user(id):
|
||||
def user_item(id):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='user', endpoint_args={'id': -1})
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='user_item', endpoint_args={'id': -1})
|
||||
|
||||
users_query = db.session.query(User).order_by(User.first_name, User.last_name)
|
||||
users = eager_load_user_relationships(users_query).all()
|
||||
|
|
|
|||
|
|
@ -17,18 +17,19 @@ def list_worklog():
|
|||
return render_template(
|
||||
'table.html',
|
||||
header=worklog_headers,
|
||||
rows=[{"id": log.id, "cells": [fn(log) for fn in worklog_headers.values()]} for log in query.all()],
|
||||
model_name='worklog',
|
||||
title="Work Log",
|
||||
entry_route='worklog_entry',
|
||||
fields = ['contact.identifier', 'work_item.identifier', 'start_time', 'end_time', 'complete', 'followup', 'analysis'],
|
||||
entry_route='worklog_item',
|
||||
csv_route='worklog'
|
||||
)
|
||||
|
||||
@main.route("/worklog/<id>")
|
||||
def worklog_entry(id):
|
||||
def worklog_item(id):
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_entry', endpoint_args={'id': -1})
|
||||
return render_template('error.html', title='Bad ID', message='ID must be an integer.', endpoint='worklog_item', endpoint_args={'id': -1})
|
||||
|
||||
log = eager_load_worklog_relationships(db.session.query(WorkLog)).get(id)
|
||||
user_query = db.session.query(User).order_by(User.first_name)
|
||||
|
|
@ -55,7 +56,7 @@ def worklog_entry(id):
|
|||
items=items
|
||||
)
|
||||
|
||||
@main.route("/worklog_entry/new", methods=["GET"])
|
||||
@main.route("/worklog_item/new", methods=["GET"])
|
||||
def new_worklog():
|
||||
items = eager_load_inventory_relationships(db.session.query(Inventory)).all()
|
||||
users = eager_load_user_relationships(db.session.query(User).order_by(User.first_name)).all()
|
||||
|
|
|
|||
31
inventory/static/js/table.js
Normal file
31
inventory/static/js/table.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
function Table(cfg) {
|
||||
return {
|
||||
id: cfg.id,
|
||||
refreshUrl: cfg.refreshUrl,
|
||||
headers: cfg.headers || [],
|
||||
perPage: cfg.perPage || 10,
|
||||
offset: cfg.offset || 0,
|
||||
fields: cfg.fields || [],
|
||||
|
||||
init() {
|
||||
if (this.refreshUrl) this.refresh();
|
||||
},
|
||||
|
||||
buildRefreshUrl() {
|
||||
if (!this.refreshUrl) return null;
|
||||
const u = new URL(this.refreshUrl, window.location.origin);
|
||||
u.search = new URLSearchParams({ view: 'table', offset: this.offset, 'limit': this.perPage, 'fields': this.fields }).toString();
|
||||
return u.toString();
|
||||
},
|
||||
|
||||
async refresh() {
|
||||
const url = this.buildRefreshUrl();
|
||||
if (!url) return;
|
||||
const res = await fetch(url, { headers: { 'HX-Request': 'true' } });
|
||||
const text = await res.text();
|
||||
if (this.$refs.body) {
|
||||
this.$refs.body.innerHTML = text;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
29
inventory/templates/fragments/_table_data_fragment.html
Normal file
29
inventory/templates/fragments/_table_data_fragment.html
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!-- Table Data Fragment -->
|
||||
{#
|
||||
<table>
|
||||
<thead>
|
||||
{% if headers %}
|
||||
<tr>
|
||||
{% for header in headers %}
|
||||
<th>{{ header }}</th>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for col in rows[0].keys() %}
|
||||
<th>{{ col }}</th>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
#}
|
||||
{% for r in rows %}
|
||||
<tr style="cursor: pointer;" onclick="window.location='{{ url_for('main.' + model_name + '_item', id=r.id) }}'">
|
||||
{% for key, val in r.items() if not key == 'id' %}
|
||||
<td class="text-nowrap">{{ val if val else '-' }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{#
|
||||
</tbody>
|
||||
</table>
|
||||
#}
|
||||
|
|
@ -51,14 +51,21 @@
|
|||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro dynamic_table(id, headers=none, rows=none, entry_route=None, title=None, per_page=15) %}
|
||||
{% macro dynamic_table(id, headers=none, rows=none, fields=none, entry_route=None, title=None, per_page=15, offset=0, refresh_url=none) %}
|
||||
<!-- Table Fragment -->
|
||||
|
||||
{% if rows %}
|
||||
{% if rows or refresh_url %}
|
||||
{% if title %}
|
||||
<label for="datatable-{{ id|default('table')|replace(' ', '-')|lower }}" class="form-label">{{ title }}</label>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive" id="table-container-{{ id }}" x-data='Table({
|
||||
id: "{{ id }}",
|
||||
refreshUrl: {{ refresh_url|tojson if refresh_url else "null" }},
|
||||
headers: {{ headers|tojson if headers else "[]" }},
|
||||
perPage: {{ per_page }},
|
||||
offset: {{ offset if offset else 0 }},
|
||||
fields: {{ fields|tojson if fields else "[]" }},
|
||||
})'>
|
||||
<table id="datatable-{{ id|default('table')|replace(' ', '-')|lower }}"
|
||||
class="table table-bordered table-sm table-hover table-striped table-light m-0{% if title %} caption-top{% endif %}">
|
||||
<thead class="sticky-top">
|
||||
|
|
@ -68,7 +75,8 @@
|
|||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody x-ref="body">
|
||||
{% if rows %}
|
||||
{% for row in rows %}
|
||||
<tr {% if entry_route %}onclick="window.location='{{ url_for('main.' + entry_route, id=row.id) }}'"
|
||||
style="cursor: pointer;"{% endif %}{% if row['highlight'] %} class="table-info"{% endif %}>
|
||||
|
|
@ -85,6 +93,7 @@
|
|||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@
|
|||
id='owner',
|
||||
label='Contact',
|
||||
current_item=item.owner,
|
||||
entry_link='user',
|
||||
entry_link='user_item',
|
||||
enabled=item.condition not in ["Removed", "Disposed"],
|
||||
refresh_url=url_for('ui.list_items', model_name='user'),
|
||||
select_url=url_for('ui.update_item', model_name='inventory'),
|
||||
|
|
@ -278,7 +278,7 @@
|
|||
<div class="col overflow-auto" style="max-height: 300px;">
|
||||
{% for note in notes %}
|
||||
{% set title %}
|
||||
{{ note.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ links.entry_link('worklog_entry', note.work_log_id) }}
|
||||
{{ note.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}{{ links.entry_link('worklog_item', note.work_log_id) }}
|
||||
{% endset %}
|
||||
{{ editor.render_editor(
|
||||
id = 'updates' + (note.id | string),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
<script src="{{ url_for('static', filename='js/editor.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/image.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/label.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/table.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/toast.js') }}" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO"
|
||||
|
|
|
|||
|
|
@ -26,5 +26,13 @@
|
|||
) }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{{ tables.dynamic_table(headers=header, rows=rows, id='table', entry_route=entry_route) }}
|
||||
{{ fields }}
|
||||
{{ tables.dynamic_table(
|
||||
id='table',
|
||||
headers=header.keys()|list if header else [],
|
||||
entry_route=entry_route,
|
||||
refresh_url = url_for('ui.list_items', model_name=model_name, view='table'),
|
||||
offset=offset,
|
||||
fields=fields
|
||||
) }}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
id='supervisor',
|
||||
label='Supervisor',
|
||||
current_item=user.supervisor if user.supervisor else None,
|
||||
entry_link='user',
|
||||
entry_link='user_item',
|
||||
enabled=user.active,
|
||||
refresh_url = url_for('ui.list_items', model_name='user'),
|
||||
select_url = url_for('ui.update_item', model_name='user'),
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
{% endset %}
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
{{ tables.render_table(headers=worklog_headers, rows=worklog_rows, id='worklog', entry_route='worklog_entry', title=worklog_title, per_page=8) }}
|
||||
{{ tables.render_table(headers=worklog_headers, rows=worklog_rows, id='worklog', entry_route='worklog_item', title=worklog_title, per_page=8) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@
|
|||
id='contact',
|
||||
label='Contact',
|
||||
current_item=log.contact,
|
||||
entry_link='user',
|
||||
entry_link='user_item',
|
||||
enabled = not log.complete,
|
||||
refresh_url=url_for('ui.list_items', model_name='user'),
|
||||
select_url=url_for('ui.update_item', model_name='worklog'),
|
||||
|
|
|
|||
|
|
@ -112,12 +112,16 @@ def list_items(model_name):
|
|||
for r in rows
|
||||
]
|
||||
|
||||
print(items)
|
||||
want_option = (request.args.get("view") == "option")
|
||||
want_list = (request.args.get("view") == "list")
|
||||
want_table = (request.args.get("view") == "table")
|
||||
if want_option:
|
||||
return render_template("fragments/_option_fragment.html", options=items)
|
||||
if want_list:
|
||||
return render_template("fragments/_list_fragment.html", options=items)
|
||||
if want_table:
|
||||
return render_template("fragments/_table_data_fragment.html", rows=items, model_name=model_name)
|
||||
return jsonify({"items": items})
|
||||
|
||||
@bp.post("/<model_name>/create")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue