Compare commits

...

2 commits

13 changed files with 139 additions and 88 deletions

View file

@ -74,10 +74,10 @@ class Inventory(db.Model, ImageAttachable):
parts.append(f"notes={repr(self.notes)}")
if self.owner:
parts.append(f"owner={repr(self.owner.full_name)}")
parts.append(f"owner={repr(self.owner.identifier)}")
if self.location:
parts.append(f"location={repr(self.location.full_name)}")
parts.append(f"location={repr(self.location.identifier)}")
return f"<Inventory({', '.join(parts)})>"

View file

@ -35,7 +35,7 @@ class Room(ValidatableMixin, db.Model):
return f"<Room(id={self.id}, room={repr(self.name)}, area_id={self.area_id}, function_id={self.function_id})>"
@property
def full_name(self):
def identifier(self):
name = self.name or ""
func = self.room_function.description if self.room_function else ""
return f"{name} - {func}".strip(" -")

View file

@ -31,7 +31,7 @@ class User(db.Model, ImageAttachable):
image: Mapped[Optional['Image']] = relationship('Image', back_populates='user', passive_deletes=True)
@property
def full_name(self) -> str:
def identifier(self) -> str:
return f"{self.first_name or ''} {self.last_name or ''}".strip()
def __init__(self, first_name: Optional[str] = None, last_name: Optional[str] = None,

View file

@ -20,8 +20,8 @@ inventory_headers = {
"Model": lambda i: {"text": i.model},
"Item Type": lambda i: {"text": i.item.description} if i.item 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.full_name, "url": url_for("main.user", id=i.owner.id)} if i.owner else {"text": None},
"Location": lambda i: {"text": i.location.full_name} if i.location else {"Text": None},
"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}
}
@ -52,14 +52,14 @@ FILTER_MAP = {
user_headers = {
"Last Name": lambda i: {"text": i.last_name},
"First Name": lambda i: {"text": i.first_name},
"Supervisor": lambda i: {"text": i.supervisor.full_name, "url": url_for("main.user", id=i.supervisor.id)} if i.supervisor else {"text": None},
"Location": lambda i: {"text": i.location.full_name} if i.location else {"text": None},
"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}
}
worklog_headers = {
"Contact": lambda i: {"text": i.contact.full_name, "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", 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},

View file

@ -61,10 +61,10 @@ def index():
users = set([log.contact for log in worklog_query if log.contact])
work_summary = {}
for user in sorted(users, key=lambda u: u.full_name):
work_summary[user.full_name] = {}
work_summary[user.full_name]['active_count'] = len([log for log in worklog_query if log.contact == user and not log.complete])
work_summary[user.full_name]['complete_count'] = len([log for log in worklog_query if log.contact == user and log.complete])
for user in sorted(users, key=lambda u: u.identifier):
work_summary[user.identifier] = {}
work_summary[user.identifier]['active_count'] = len([log for log in worklog_query if log.contact == user and not log.complete])
work_summary[user.identifier]['complete_count'] = len([log for log in worklog_query if log.contact == user and log.complete])
datasets['work_summary'] = [{
'type': 'bar',

View file

@ -31,11 +31,11 @@ def list_inventory():
if filter_by == 'user':
if not (user := db.session.query(User).filter(User.id == id).first()):
return "Invalid User ID", 400
filter_name = user.full_name
filter_name = user.identifier
elif filter_by == 'location':
if not (room := db.session.query(Room).filter(Room.id == id).first()):
return "Invalid Location ID", 400
filter_name = room.full_name
filter_name = room.identifier
else:
if not (item := db.session.query(Item).filter(Item.id == id).first()):
return "Invalid Type ID", 400
@ -49,7 +49,7 @@ def list_inventory():
inventory = sorted(inventory, key=lambda i: i.identifier)
return render_template(
'table.html',
'table.html',
title=f"Inventory Listing ({filter_name})" if filter_by else "Inventory Listing",
breadcrumb=[{'label': 'Inventory', 'url': url_for('main.inventory_index')}],
header=inventory_headers,
@ -79,11 +79,11 @@ def inventory_index():
listing = chunk_list(types, 12)
elif category:
return f"Dude, why {category}?"
return render_template(
'inventory_index.html',
title=f"Inventory ({category.capitalize()} Index)" if category else "Inventory",
category=category,
'inventory_index.html',
title=f"Inventory ({category.capitalize()} Index)" if category else "Inventory",
category=category,
listing=listing
)
@ -109,14 +109,14 @@ def inventory_item(id):
title = f"Inventory Record - {item.identifier}"
else:
title = "Inventory Record - Not Found"
return render_template('error.html',
return render_template('error.html',
title=title,
message=f'Inventory item with id {id} not found!',
endpoint='inventory_item',
endpoint_args={'id': -1})
return render_template("inventory.html", title=title, item=item,
brands=brands, users=users, rooms=rooms,
return render_template("inventory.html", title=title, item=item,
brands=brands, users=users, rooms=rooms,
worklog=worklog,
worklog_headers=filtered_worklog_headers,
worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog],
@ -147,7 +147,7 @@ def new_inventory_item():
worklog=[],
worklog_headers={},
worklog_rows=[]
)
)
@main.route("/api/inventory", methods=["POST"])
def create_inventory_item():
@ -160,11 +160,11 @@ def create_inventory_item():
db.session.commit()
return jsonify({"success": True, "id": new_item.id}), 201
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@main.route("/api/inventory/<int:id>", methods=["PUT"])
def update_inventory_item(id):
try:
@ -194,7 +194,7 @@ def update_inventory_item(id):
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@main.route("/api/inventory/<int:id>", methods=["DELETE"])
def delete_inventory_item(id):
try:
@ -202,16 +202,16 @@ def delete_inventory_item(id):
if not item:
return jsonify({"success": False, "error": f"Item with ID {id} not found"}), 404
db.session.delete(item)
db.session.commit()
return jsonify({"success": True}), 200
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@main.route("/api/inventory/export", methods=["POST"])
def get_inventory_csv():
def export_value(item, col):
@ -220,24 +220,24 @@ def get_inventory_csv():
case "brand":
return item.brand.name
case "location":
return item.location.full_name
return item.location.identifier
case "owner":
return item.owner.full_name
return item.owner.identifier
case "type":
return item.item.description
case _:
return getattr(item, col, "")
except Exception:
return ""
data = request.get_json()
ids = data.get('ids', [])
if not ids:
return jsonify({"success": False, "error": "No IDs provided"}), 400
rows = eager_load_inventory_relationships(db.session.query(Inventory).filter(Inventory.id.in_(ids))).all()
columns = [
"id",
"timestamp",
@ -259,7 +259,7 @@ def get_inventory_csv():
@main.route("/inventory_available")
def inventory_available():
query = eager_load_inventory_relationships(db.session.query(Inventory).filter(Inventory.condition == "Working"))
inventory = query.all()
inventory = sorted(inventory, key=lambda i: i.identifier)

View file

@ -40,9 +40,9 @@ def user(id):
.filter(Inventory.owner_id == id) # type: ignore
.filter(Inventory.condition.in_(ACTIVE_STATUSES))
)
inventory = inventory_query.all()
filtered_inventory_headers = {k: v for k, v in inventory_headers.items() if k not in ['Date Entered', 'Name', 'Serial Number',
filtered_inventory_headers = {k: v for k, v in inventory_headers.items() if k not in ['Date Entered', 'Name', 'Serial Number',
'Bar Code', 'Condition', 'Owner', 'Notes',
'Brand', 'Model', 'Shared?', 'Location']}
worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.contact_id == id)
@ -50,7 +50,7 @@ def user(id):
filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Contact', 'Follow Up?', 'Quick Analysis?']}
if user:
title = f"User Record - {user.full_name}" if user.active else f"User Record - {user.full_name} (Inactive)"
title = f"User Record - {user.identifier}" if user.active else f"User Record - {user.identifier} (Inactive)"
else:
title = f"User Record - User Not Found"
return render_template(
@ -60,9 +60,9 @@ def user(id):
)
return render_template(
"user.html",
title=title,
user=user, users=users, rooms=rooms, assets=inventory,
"user.html",
title=title,
user=user, users=users, rooms=rooms, assets=inventory,
inventory_headers=filtered_inventory_headers,
inventory_rows=[{"id": item.id, "cells": [fn(item) for fn in filtered_inventory_headers.values()]} for item in inventory],
worklog=worklog,
@ -98,7 +98,7 @@ def create_user():
db.session.commit()
return jsonify({"success": True, "id": new_user.id}), 201
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@ -111,7 +111,7 @@ def update_user(id):
if not user:
return jsonify({"success": False, "error": f"User with ID {id} not found."}), 404
user.staff = bool(data.get("staff", user.staff))
user.active = bool(data.get("active", user.active))
user.last_name = data.get("last_name", user.last_name)
@ -122,33 +122,33 @@ def update_user(id):
db.session.commit()
return jsonify({"success": True, "id": user.id}), 200
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@main.route("/api/user/export", methods=["POST"])
def get_user_csv():
def export_value(user, col):
try:
match col:
case "location":
return user.location.full_name
return user.location.identifier
case "supervisor":
return user.supervisor.full_name
return user.supervisor.identifier
case _:
return getattr(user, col, "")
except Exception:
return ""
data = request.get_json()
ids = data.get('ids', [])
if not ids:
return jsonify({"success": False, "error": "No IDs provided"}), 400
rows = eager_load_user_relationships(db.session.query(User).filter(User.id.in_(ids))).all()
columns = [
"id",
"staff",

View file

@ -13,7 +13,7 @@ from ..utils.load import eager_load_worklog_relationships, eager_load_user_relat
@main.route("/worklog")
def list_worklog():
query = eager_load_worklog_relationships(db.session.query(WorkLog))
query = eager_load_worklog_relationships(db.session.query(WorkLog))
return render_template(
'table.html',
header=worklog_headers,
@ -48,10 +48,10 @@ def worklog_entry(id):
)
return render_template(
"worklog.html",
title=title,
log=log,
users=users,
"worklog.html",
title=title,
log=log,
users=users,
items=items
)
@ -86,7 +86,7 @@ def create_worklog():
db.session.commit()
return jsonify({"success": True, "id": new_worklog.id}), 201
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@ -110,7 +110,7 @@ def update_worklog(id):
existing = {str(note.id): note for note in log.updates}
incoming = data.get("updates", [])
new_updates = []
for note_data in incoming:
if isinstance(note_data, dict):
if "id" in note_data and str(note_data["id"]) in existing:
@ -119,13 +119,13 @@ def update_worklog(id):
new_updates.append(note)
elif "content" in note_data:
new_updates.append(WorkNote(content=note_data["content"]))
log.updates[:] = new_updates # This replaces in-place
db.session.commit()
return jsonify({"success": True, "id": log.id}), 200
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@ -137,23 +137,23 @@ def delete_worklog(id):
if not log:
return jsonify({"success": False, "errpr": f"Item with ID {id} not found!"}), 404
db.session.delete(log)
db.session.commit()
return jsonify({"success": True}), 200
except Exception as e:
db.session.rollback()
return jsonify({"success": False, "error": str(e)}), 400
@main.route("/api/worklog/export", methods=["POST"])
def get_worklog_csv():
def export_value(log, col):
try:
match col:
case "contact":
return log.contact.full_name
return log.contact.identifier
case "work_item":
return log.work_item.identifier
case "latest_update":
@ -164,15 +164,15 @@ def get_worklog_csv():
return getattr(log, col, "")
except Exception:
return ""
data = request.get_json()
ids = data.get('ids', [])
if not ids:
return jsonify({"success": False, "error": "No IDs provided"}), 400
rows = eager_load_worklog_relationships(db.session.query(WorkLog).filter(WorkLog.id.in_(ids))).all()
columns = [
"id",
"start_time",

View file

@ -0,0 +1,57 @@
{% macro render_dropdown(id, list, label, current_item = None, entry_link = None) %}
<label for="{{ id }}" class="form-label">
{{ label }}
{% if entry_link %}
{{ links.entry_link(entry_link, current_item.id) }}
{% endif %}
</label>
<div class="dropdown">
<button class="btn btn-outline-dark dropdown-toggle w-100" type="button" data-bs-toggle="dropdown"
data-inv-value="{{ current_item.id if current_item else '' }}" id="{{ id }}Button">
{{ current_item.identifier if current_item else '-' }}
</button>
<input type="hidden" name="{{ id }}" id="{{ id }}" value="{{ current_item.id if current_item else '' }}">
<ul class="dropdown-menu w-100" id="menu{{ id }}">
<input type="text" class="form-control" id="search{{ id }}" placeholder="Search...">
{% for item in list %}
<li><a class="dropdown-item" data-inv-value="{{ item.id }}" onclick="{{ id }}SetButton({{ item.id }}, '{{ item.identifier }}')">{{ item.identifier }}</a></li>
{% endfor %}
</ul>
</div>
<script>
function {{ id }}SetButton(id, identifier) {
const button = document.getElementById("{{ id }}Button");
const input = document.getElementById("{{ id }}");
button.dataset.invValue = id;
button.textContent = identifier;
input.value = id;
console.log("Selected {{ id }} ID:", id);
}
document.addEventListener("DOMContentLoaded", () => {
const {{ id }}Dropdown = document.getElementById("menu{{ id }}");
const {{ id }}Input = document.getElementById("{{ id }}");
{{ id }}Dropdown.addEventListener("click", (e) => {
if (e.target.tagName === "A") {
{{ id }}Input.value = e.target.dataset.invValue;
console.log("Selected {{ id }} ID:", {{ id }}Input.value);
}
});
const {{ id }}SearchInput = document.getElementById("search{{ id }}");
console.log({{ id }}SearchInput);
{{ id }}SearchInput.addEventListener("input", () => {
const filter = {{ id }}SearchInput.value.toLowerCase();
const items = {{ id }}Dropdown.querySelectorAll("a.dropdown-item");
items.forEach(item => {
if (item.textContent.toLowerCase().includes(filter)) {
item.style.display = "";
} else {
item.style.display = "none";
}
});
});
});
</script>
{% endmacro %}

View file

@ -209,7 +209,7 @@
<option>-</option>
{% for user in users %}
<option value="{{ user.id }}" {% if user.id==item.owner_id %} selected{% endif %}>{{
user.full_name
user.identifier
}}</option>
{% endfor %}
</select>
@ -221,7 +221,7 @@
<option>-</option>
{% for room in rooms %}
<option value="{{ room.id }}" {% if room.id==item.location_id %} selected{% endif %}>{{
room.full_name }}</option>
room.identifier }}</option>
{% endfor %}
</select>
</div>

View file

@ -1,6 +1,7 @@
{% import "fragments/_button_fragment.html" as buttons %}
{% import "fragments/_breadcrumb_fragment.html" as breadcrumbs %}
{% import "fragments/_combobox_fragment.html" as combos %}
{% import "fragments/_dropdown_fragment.html" as dropdowns %}
{% import "fragments/_editor_fragment.html" as editor %}
{% import "fragments/_icon_fragment.html" as icons %}
{% import "fragments/_image_fragment.html" as images %}

View file

@ -108,7 +108,7 @@
<option>-</option>
{% for supervisor in users %}
<option value="{{ supervisor.id }}"{% if supervisor.id==user.supervisor_id %} selected{% endif %}>
{{ supervisor.full_name }}</option>
{{ supervisor.identifier }}</option>
{% endfor %}
</select>
</div>
@ -119,7 +119,7 @@
<option>-</option>
{% for location in rooms %}
<option value="{{ location.id }}"{% if location.id==user.location_id %} selected{% endif %}>{{
location.full_name }}</option>
location.identifier }}</option>
{% endfor %}
</select>
</div>
@ -142,7 +142,7 @@
{% set inventory_title %}
Assets
{{ links.export_link(
(user.full_name | lower | replace(' ', '_')) + '_user_inventory',
(user.identifier | lower | replace(' ', '_')) + '_user_inventory',
'inventory',
{'ids': id_list}
) }}
@ -157,7 +157,7 @@
{% set worklog_title %}
Work Done
{{ links.export_link(
(user.full_name | lower | replace(' ', '_')) + '_user_worklog',
(user.identifier | lower | replace(' ', '_')) + '_user_worklog',
'worklog',
{'ids': id_list}
) }}

View file

@ -24,7 +24,7 @@
analysis: document.querySelector("input[name='analysis']").checked,
followup: document.querySelector("input[name='followup']").checked,
contact_id: parseInt(document.querySelector("select[name='contact']").value) || null,
work_item_id: parseInt(document.querySelector("select[name='item']").value) || null,
work_item_id: parseInt(document.querySelector("input[name='item']").value) || null,
updates: updates
};
@ -163,25 +163,18 @@
<option value="">-</option>
{% for contact in users %}
<option value="{{ contact.id }}" {% if contact.id==log.contact_id %} selected{% endif %}>{{
contact.full_name }}
contact.identifier }}
</option>
{% endfor %}
</select>
</div>
<div class="col-4">
<label for="item" class="form-label">
Work Item
{% if log.work_item_id %}
{{ links.entry_link('inventory_item', log.work_item_id) }}
{% endif %}
</label>
<select id="item" name="item" class="form-select"{% if log.complete %} disabled{% endif %}>
<option value="">-</option>
{% for item in items %}
<option value="{{ item.id }}" {% if item.id==log.work_item_id %} selected{% endif %}>{{ item.identifier
}}</option>
{% endfor %}
</select>
{{ dropdowns.render_dropdown(
id='item',
list=items,
label='Work Item',
current_item=log.work_item
) }}
</div>
<div class="col-4">
<div class="row">