diff --git a/routes.py b/routes.py
index 28232f0..fb797a5 100644
--- a/routes.py
+++ b/routes.py
@@ -1,90 +1,20 @@
-from flask import Blueprint, render_template, url_for, request, redirect, flash, jsonify
+from bs4 import BeautifulSoup
+from flask import render_template, url_for, request, redirect
from flask import current_app as app
-from .models import Brand, Item, Inventory, RoomFunction, User, WorkLog, Room, Area
+import pandas as pd
from sqlalchemy import or_
from sqlalchemy.orm import aliased
+
from . import db
-from .utils.load import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_room_relationships, eager_load_worklog_relationships, chunk_list, add_named_entities
-import pandas as pd
-import traceback
-import json
-import datetime
-from bs4 import BeautifulSoup
-
-main = Blueprint('main', __name__)
-
-checked_box = '''
-
-'''
-unchecked_box = ''
-
-ACTIVE_STATUSES = [
- "Working",
- "Deployed",
- "Partially Inoperable",
- "Unverified"
-]
-
-INACTIVE_STATUSES = [
- "Inoperable",
- "Removed",
- "Disposed"
-]
-
-inventory_headers = {
- "Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
- "Identifier": lambda i: {"text": i.identifier},
- "Inventory #": lambda i: {"text": i.inventory_name},
- "Serial #": 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.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},
- "Condition": lambda i: {"text": i.condition},
- # "Notes": lambda i: {"text": i.notes}
-}
-
-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},
- "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},
- "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},
- "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, "highlight": i.followup},
- "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box},
-}
-
-worklog_form_fields = {
- "start": lambda log: {"label": "Start Timestamp", "type": "date", "value": log.start_time.date().isoformat() if log.start_time else ""},
- "end": lambda log: {"label": "End Timestamp", "type": "date", "value": log.end_time.date().isoformat() if log.end_time else ""},
- "contact": lambda log: {"label": "Contact", "type": "datalist", "value": log.contact.full_name if log.contact else "", "list": "contactList"},
- "item": lambda log: {"label": "Work Item", "type": "datalist", "value": log.work_item.identifier if log.work_item else "", "list": "itemList"},
- "complete": lambda log: {"label": "Complete?", "type": "checkbox", "value": log.complete},
- "followup": lambda log: {"label": "Follow Up?", "type": "checkbox", "value": log.followup},
- "analysis": lambda log: {"label": "Quick Analysis?", "type": "checkbox", "value": log.analysis},
- "notes": lambda log: {"label": "Notes", "type": "textarea", "value": log.notes or "", "rows": 15}
-}
+from .models import Inventory, User, WorkLog
+from .routes import main
+from .routes.helpers import worklog_headers, inventory_headers, user_headers
+from .utils.load import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_worklog_relationships
@main.after_request
def prettify_html_response(response):
if app.debug and response.content_type.startswith("text/html"):
try:
- from bs4 import BeautifulSoup
-
soup = BeautifulSoup(response.get_data(as_text=True), 'html5lib')
pretty_html = soup.prettify()
@@ -94,555 +24,3 @@ def prettify_html_response(response):
except Exception as e:
print(f"⚠️ Prettifying failed: {e}")
return response
-
-@main.route("/")
-def index():
- worklog_query = eager_load_worklog_relationships(
- db.session.query(WorkLog)
- ).filter(
- (WorkLog.complete == False)
- )
-
- active_worklogs = worklog_query.all()
-
- active_count = len(active_worklogs)
- active_worklog_headers = {
- k: v for k, v in worklog_headers.items()
- if k not in ['End Time', 'Quick Analysis?', 'Complete?', 'Follow Up?']
- }
-
- inventory_query = eager_load_inventory_relationships(
- db.session.query(Inventory)
- )
-
- results = inventory_query.all()
-
- data = [{
- 'id': item.id,
- 'condition': item.condition
- } for item in results]
-
- df = pd.DataFrame(data)
-
- # Count items per condition
- expected_conditions = [
- 'Deployed','Inoperable', 'Partially Inoperable',
- 'Unverified', 'Working'
- ]
- print(df)
- if 'condition' in df.columns:
- pivot = df['condition'].value_counts().reindex(expected_conditions, fill_value=0)
- else:
- pivot = pd.Series([0] * len(expected_conditions), index=expected_conditions)
-
- # Convert pandas/numpy int64s to plain old Python ints
- pivot = pivot.astype(int)
- labels = list(pivot.index)
- data = [int(x) for x in pivot.values]
-
- datasets = [{
- 'type': 'pie',
- 'labels': labels,
- 'values': data,
- '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(
- "index.html",
- active_count=active_count,
- active_worklog_headers=active_worklog_headers,
- active_worklog_rows=active_worklog_rows,
- labels=labels,
- datasets=datasets
- )
-
-def link(text, endpoint, **values):
- return {"text": text, "url": url_for(endpoint, **values)}
-
-FILTER_MAP = {
- 'user': Inventory.owner_id,
- 'location': Inventory.location_id,
- 'type': Inventory.type_id,
-}
-
-@main.route("/inventory")
-def list_inventory():
- filter_by = request.args.get('filter_by', type=str)
- id = request.args.get('id', type=int)
-
- filter_name = None
-
- query = db.session.query(Inventory)
- query = eager_load_inventory_relationships(query)
- query = query.order_by(Inventory.inventory_name, Inventory.barcode, Inventory.serial)
-
- if filter_by and id:
- column = FILTER_MAP.get(filter_by)
- if column is not None:
- filter_name = None
- 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
- 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
- else:
- if not (item := db.session.query(Item).filter(Item.id == id).first()):
- return "Invalid Type ID", 400
- filter_name = item.description
-
- query = query.filter(column == id)
- else:
- return "Invalid filter_by parameter", 400
-
- inventory = query.all()
-
- return render_template(
- '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,
- rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory],
- entry_route = 'inventory_item'
- )
-
-@main.route("/inventory/index")
-def inventory_index():
- category = request.args.get('category')
- listing = None
-
- if category == 'user':
- users = db.session.query(User.id, User.first_name, User.last_name).order_by(User.first_name, User.last_name).all()
- listing = chunk_list([(user.id, f"{user.first_name or ''} {user.last_name or ''}".strip()) for user in users], 12)
- elif category == 'location':
- rooms = (
- db.session.query(Room.id, Room.name, RoomFunction.description)
- .join(RoomFunction, Room.function_id == RoomFunction.id)
- .order_by(Room.name, RoomFunction.description)
- .all()
- )
- listing = chunk_list([(room.id, f"{room.name or ''} - {room.description or ''}".strip()) for room in rooms], 12)
- elif category == 'type':
- types = db.session.query(Item.id, Item.description).order_by(Item.description).all()
- 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,
- listing=listing
- )
-
-@main.route("/inventory_item/", methods=['GET', 'POST'])
-def inventory_item(id):
- try:
- id = int(id)
- except ValueError:
- return render_template('error.html', title="Bad ID", message="ID must be an integer", endpoint='inventory_item', endpoint_args={'id': -1})
-
- inventory_query = db.session.query(Inventory)
- item = eager_load_inventory_relationships(inventory_query).filter(Inventory.id == id).first()
- brands = db.session.query(Brand).all()
- users = eager_load_user_relationships(db.session.query(User)).all()
- rooms = eager_load_room_relationships(db.session.query(Room)).all()
- worklog_query = db.session.query(WorkLog).filter(WorkLog.work_item_id == id)
- worklog = eager_load_worklog_relationships(worklog_query).all()
- types = db.session.query(Item).all()
- filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Work Item', 'Contact', 'Follow Up?', 'Quick Analysis?']}
-
- if item:
- title = f"Inventory Record - {item.identifier}"
- else:
- title = "Inventory Record - Not Found"
- 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,
- 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],
- types=types
- )
-
-@main.route("/inventory_item/new", methods=["GET"])
-def new_inventory_item():
- brands = db.session.query(Brand).all()
- users = eager_load_user_relationships(db.session.query(User)).all()
- rooms = eager_load_room_relationships(db.session.query(Room)).all()
- types = db.session.query(Item).all()
-
- item = Inventory(
- timestamp=datetime.datetime.now(),
- condition="Unverified",
- needed="",
- type_id=None,
- )
-
- return render_template(
- "inventory.html",
- item=item,
- brands=brands,
- users=users,
- rooms=rooms,
- types=types,
- worklog=[],
- worklog_headers={},
- worklog_rows=[]
- )
-
-@main.route("/api/inventory", methods=["POST"])
-def create_inventory_item():
- try:
- data = request.get_json(force=True)
-
- new_item = Inventory.from_dict(data)
-
- db.session.add(new_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/", methods=["PUT"])
-def update_inventory_item(id):
- try:
- data = request.get_json(force=True)
- item = db.session.query(Inventory).get(id)
-
- if not item:
- return jsonify({"success": False, "error": f"Inventory item with ID {id} not found."}), 404
-
- item.timestamp = datetime.datetime.fromisoformat(data.get("timestamp")) if data.get("timestamp") else item.timestamp
- item.condition = data.get("condition", item.condition)
- item.needed = data.get("needed", item.needed)
- item.type_id = data.get("type_id", item.type_id)
- item.inventory_name = data.get("inventory_name", item.inventory_name)
- item.serial = data.get("serial", item.serial)
- item.model = data.get("model", item.model)
- item.notes = data.get("notes", item.notes)
- item.owner_id = data.get("owner_id", item.owner_id)
- item.brand_id = data.get("brand_id", item.brand_id)
- item.location_id = data.get("location_id", item.location_id)
- item.barcode = data.get("barcode", item.barcode)
- item.shared = bool(data.get("shared", item.shared))
-
- db.session.commit()
-
- return jsonify({"success": True, "id": item.id}), 200
-
- except Exception as e:
- db.session.rollback()
- return jsonify({"success": False, "error": str(e)}), 400
-
-@main.route("/api/inventory/", methods=["DELETE"])
-def delete_inventory_item(id):
- try:
- item = db.session.query(Inventory).get(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("/users")
-def list_users():
- query = eager_load_user_relationships(db.session.query(User)).order_by(User.last_name, User.first_name)
- users = query.all()
- 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],
- title = "Users",
- entry_route = 'user'
- )
-
-@main.route("/user/")
-def user(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})
-
- users_query = db.session.query(User).order_by(User.first_name, User.last_name)
- users = eager_load_user_relationships(users_query).all()
- user = next((u for u in users if u.id == id), None)
- rooms_query = db.session.query(Room)
- rooms = eager_load_room_relationships(rooms_query).all()
- inventory_query = (
- eager_load_inventory_relationships(db.session.query(Inventory))
- .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', 'Inventory #', 'Serial #',
- 'Bar Code #', 'Condition', 'Owner', 'Notes',
- 'Brand', 'Model', 'Shared?', 'Location']}
- worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.contact_id == id)
- worklog = worklog_query.order_by(WorkLog.start_time.desc()).all()
- 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)"
- else:
- title = f"User Record - User Not Found"
- return render_template(
- 'error.html',
- title=title,
- message=f"User with id {id} not found!"
- )
-
- return render_template(
- "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,
- worklog_headers=filtered_worklog_headers,
- worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog]
- )
-
-@main.route("/worklog")
-def list_worklog(page=1):
- page = request.args.get('page', default=1, type=int)
- query = eager_load_worklog_relationships(db.session.query(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()],
- title="Work Log",
- entry_route='worklog_entry'
- )
-
-@main.route("/worklog/")
-def worklog_entry(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})
-
- log = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.id == id).first()
- user_query = db.session.query(User)
- users = eager_load_user_relationships(user_query).all()
- item_query = db.session.query(Inventory)
- items = eager_load_inventory_relationships(item_query).all()
-
- if log:
- title = f'Work Log - Entry #{id}'
- else:
- title = "Work Log - Entry Not Found"
- return render_template(
- 'error.html',
- title=title,
- message=f"The work log with ID {id} is not found!"
- )
-
- return render_template(
- "worklog.html",
- title=title,
- log=log,
- users=users,
- items=items,
- form_fields=worklog_form_fields
- )
-
-@main.route("/search")
-def search():
- query = request.args.get('q', '').strip()
-
- if not query:
- return redirect(url_for('main.index'))
-
- InventoryAlias = aliased(Inventory)
- UserAlias = aliased(User)
-
- inventory_query = eager_load_inventory_relationships(db.session.query(Inventory).join(UserAlias, Inventory.owner)).filter(
- or_(
- Inventory.inventory_name.ilike(f"%{query}%"),
- Inventory.serial.ilike(f"%{query}%"),
- Inventory.barcode.ilike(f"%{query}%"),
- Inventory.notes.ilike(f"%{query}%"),
- UserAlias.first_name.ilike(f"%{query}%"),
- UserAlias.last_name.ilike(f"%{query}%")
- ))
- inventory_results = inventory_query.all()
- user_query = eager_load_user_relationships(db.session.query(User).join(UserAlias, User.supervisor)).filter(
- or_(
- User.first_name.ilike(f"%{query}%"),
- User.last_name.ilike(f"%{query}%"),
- UserAlias.first_name.ilike(f"%{query}%"),
- UserAlias.last_name.ilike(f"%{query}%")
- ))
- user_results = user_query.all()
- worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog).join(UserAlias, WorkLog.contact).join(InventoryAlias, WorkLog.work_item)).filter(
- or_(
- WorkLog.notes.ilike(f"%{query}%"),
- UserAlias.first_name.ilike(f"%{query}%"),
- UserAlias.last_name.ilike(f"%{query}%"),
- InventoryAlias.inventory_name.ilike(f"%{query}%"),
- InventoryAlias.serial.ilike(f"%{query}%"),
- InventoryAlias.barcode.ilike(f"%{query}%")
- ))
- worklog_results = worklog_query.all()
-
- results = {
- 'inventory': {
- 'results': inventory_query,
- 'headers': inventory_headers,
- 'rows': [{"id": item.id, "cells": [fn(item) for fn in inventory_headers.values()]} for item in inventory_results]
- },
- 'users': {
- 'results': user_query,
- 'headers': user_headers,
- 'rows': [{"id": user.id, "cells": [fn(user) for fn in user_headers.values()]} for user in user_results]
- },
- 'worklog': {
- 'results': worklog_query,
- 'headers': worklog_headers,
- 'rows': [{"id": log.id, "cells": [fn(log) for fn in worklog_headers.values()]} for log in worklog_results]
- }
- }
-
- return render_template('search.html', title=f"Database Search ({query})" if query else "Database Search", results=results, query=query)
-
-@main.route('/settings', methods=['GET', 'POST'])
-def settings():
- if request.method == 'POST':
- print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️")
- form = request.form
- print("📝 Raw form payload:", form)
-
- try:
- state = json.loads(form['formState'])
- import pprint
- print("🧠 Parsed state:")
- pprint.pprint(state, indent=2, width=120)
- except Exception:
- flash("Invalid form state submitted. JSON decode failed.", "danger")
- traceback.print_exc()
- return redirect(url_for('main.settings'))
-
- try:
- with db.session.begin():
- # Sync each table and grab temp ID maps
- brand_map = Brand.sync_from_state(state.get("brands", []))
- type_map = Item.sync_from_state(state.get("types", []))
- section_map = Area.sync_from_state(state.get("sections", []))
- function_map = RoomFunction.sync_from_state(state.get("functions", []))
-
- # Fix up room foreign keys based on real IDs
- submitted_rooms = []
- for room in state.get("rooms", []):
- room = dict(room) # shallow copy
- sid = room.get("section_id")
- fid = room.get("function_id")
-
- if sid is not None:
- sid_key = str(sid)
- if sid_key in section_map:
- room["section_id"] = section_map[sid_key]
-
- if fid is not None:
- fid_key = str(fid)
- if fid_key in function_map:
- room["function_id"] = function_map[fid_key]
-
- submitted_rooms.append(room)
-
- Room.sync_from_state(
- submitted_rooms=submitted_rooms,
- section_map=section_map,
- function_map=function_map
- )
-
- print("✅ COMMIT executed.")
- flash("Changes saved.", "success")
-
- except Exception as e:
- print("❌ COMMIT FAILED ❌")
- traceback.print_exc()
- flash(f"Error saving changes: {e}", "danger")
-
- return redirect(url_for('main.settings'))
-
- # === GET ===
- brands = db.session.query(Brand).order_by(Brand.name).all()
- types = db.session.query(Item).order_by(Item.description).all()
- sections = db.session.query(Area).order_by(Area.name).all()
- functions = db.session.query(RoomFunction).order_by(RoomFunction.description).all()
- rooms = eager_load_room_relationships(db.session.query(Room).order_by(Room.name)).all()
-
- return render_template('settings.html',
- title="Settings",
- brands=[b.serialize() for b in brands],
- types=[{"id": t.id, "name": t.description} for t in types],
- sections=[s.serialize() for s in sections],
- functions=[f.serialize() for f in functions],
- rooms=[r.serialize() for r in rooms]
- )
-
-@main.route("/api/settings", methods=["POST"])
-def api_settings():
- try:
- payload = request.get_json(force=True)
- except Exception as e:
- return jsonify({"error": "Invalid JSON"}), 400
-
- errors = []
- errors += Brand.validate_state(payload.get("brands", []))
- errors += Item.validate_state(payload.get("types", []))
- errors += Area.validate_state(payload.get("sections", []))
- errors += RoomFunction.validate_state(payload.get("functions", []))
- errors += Room.validate_state(payload.get("rooms", []))
-
- if errors:
- return jsonify({"errors": errors}), 400
-
- try:
- with db.session.begin():
- section_map = Area.sync_from_state(payload["sections"])
- function_map = RoomFunction.sync_from_state(payload["functions"])
- Brand.sync_from_state(payload["brands"])
- Item.sync_from_state(payload["types"])
- Room.sync_from_state(payload["rooms"], section_map, function_map)
- except Exception as e:
- db.session.rollback()
- return jsonify({"errors": [str(e)]}), 500
-
- return jsonify({"message": "Settings updated successfully."}), 200
diff --git a/routes/__init__.py b/routes/__init__.py
new file mode 100644
index 0000000..327bec7
--- /dev/null
+++ b/routes/__init__.py
@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+main = Blueprint('main', __name__)
+
+from . import inventory, user, worklog, settings, index, search
\ No newline at end of file
diff --git a/routes/helpers.py b/routes/helpers.py
new file mode 100644
index 0000000..c49d311
--- /dev/null
+++ b/routes/helpers.py
@@ -0,0 +1,78 @@
+from flask import url_for
+
+from ..models import Inventory
+
+inventory_headers = {
+ "Date Entered": lambda i: {"text": i.timestamp.strftime("%Y-%m-%d") if i.timestamp else None},
+ "Identifier": lambda i: {"text": i.identifier},
+ "Inventory #": lambda i: {"text": i.inventory_name},
+ "Serial #": 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.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},
+ "Condition": lambda i: {"text": i.condition},
+ # "Notes": lambda i: {"text": i.notes}
+}
+
+checked_box = '''
+
+'''
+unchecked_box = ''
+
+ACTIVE_STATUSES = [
+ "Working",
+ "Deployed",
+ "Partially Inoperable",
+ "Unverified"
+]
+
+INACTIVE_STATUSES = [
+ "Inoperable",
+ "Removed",
+ "Disposed"
+]
+
+FILTER_MAP = {
+ 'user': Inventory.owner_id,
+ 'location': Inventory.location_id,
+ 'type': Inventory.type_id,
+}
+
+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},
+ "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},
+ "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},
+ "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, "highlight": i.followup},
+ "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box},
+}
+
+worklog_form_fields = {
+ "start": lambda log: {"label": "Start Timestamp", "type": "date", "value": log.start_time.date().isoformat() if log.start_time else ""},
+ "end": lambda log: {"label": "End Timestamp", "type": "date", "value": log.end_time.date().isoformat() if log.end_time else ""},
+ "contact": lambda log: {"label": "Contact", "type": "datalist", "value": log.contact.full_name if log.contact else "", "list": "contactList"},
+ "item": lambda log: {"label": "Work Item", "type": "datalist", "value": log.work_item.identifier if log.work_item else "", "list": "itemList"},
+ "complete": lambda log: {"label": "Complete?", "type": "checkbox", "value": log.complete},
+ "followup": lambda log: {"label": "Follow Up?", "type": "checkbox", "value": log.followup},
+ "analysis": lambda log: {"label": "Quick Analysis?", "type": "checkbox", "value": log.analysis},
+ "notes": lambda log: {"label": "Notes", "type": "textarea", "value": log.notes or "", "rows": 15}
+}
+
+def link(text, endpoint, **values):
+ return {"text": text, "url": url_for(endpoint, **values)}
diff --git a/routes/index.py b/routes/index.py
new file mode 100644
index 0000000..0889011
--- /dev/null
+++ b/routes/index.py
@@ -0,0 +1,86 @@
+from flask import render_template
+import pandas as pd
+
+from . import main
+from .helpers import worklog_headers
+from .. import db
+from ..models import WorkLog, Inventory
+from ..utils.load import eager_load_worklog_relationships, eager_load_inventory_relationships
+
+@main.route("/")
+def index():
+ worklog_query = eager_load_worklog_relationships(
+ db.session.query(WorkLog)
+ ).filter(
+ (WorkLog.complete == False)
+ )
+
+ active_worklogs = worklog_query.all()
+
+ active_count = len(active_worklogs)
+ active_worklog_headers = {
+ k: v for k, v in worklog_headers.items()
+ if k not in ['End Time', 'Quick Analysis?', 'Complete?', 'Follow Up?']
+ }
+
+ inventory_query = eager_load_inventory_relationships(
+ db.session.query(Inventory)
+ )
+
+ results = inventory_query.all()
+
+ data = [{
+ 'id': item.id,
+ 'condition': item.condition
+ } for item in results]
+
+ df = pd.DataFrame(data)
+
+ # Count items per condition
+ expected_conditions = [
+ 'Deployed','Inoperable', 'Partially Inoperable',
+ 'Unverified', 'Working'
+ ]
+ print(df)
+ if 'condition' in df.columns:
+ pivot = df['condition'].value_counts().reindex(expected_conditions, fill_value=0)
+ else:
+ pivot = pd.Series([0] * len(expected_conditions), index=expected_conditions)
+
+ # Convert pandas/numpy int64s to plain old Python ints
+ pivot = pivot.astype(int)
+ labels = list(pivot.index)
+ data = [int(x) for x in pivot.values]
+
+ datasets = [{
+ 'type': 'pie',
+ 'labels': labels,
+ 'values': data,
+ '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(
+ "index.html",
+ active_count=active_count,
+ active_worklog_headers=active_worklog_headers,
+ active_worklog_rows=active_worklog_rows,
+ labels=labels,
+ datasets=datasets
+ )
\ No newline at end of file
diff --git a/routes/inventory.py b/routes/inventory.py
new file mode 100644
index 0000000..f3d751b
--- /dev/null
+++ b/routes/inventory.py
@@ -0,0 +1,207 @@
+import datetime
+from flask import request, render_template, url_for, jsonify
+
+from . import main
+from .helpers import FILTER_MAP, inventory_headers, worklog_headers
+
+from .. import db
+from ..models import Inventory, User, Room, Item, RoomFunction, Brand, WorkLog
+from ..utils.load import eager_load_inventory_relationships, eager_load_user_relationships, eager_load_worklog_relationships, eager_load_room_relationships, chunk_list
+
+@main.route("/inventory")
+def list_inventory():
+ filter_by = request.args.get('filter_by', type=str)
+ id = request.args.get('id', type=int)
+
+ filter_name = None
+
+ query = db.session.query(Inventory)
+ query = eager_load_inventory_relationships(query)
+ query = query.order_by(Inventory.inventory_name, Inventory.barcode, Inventory.serial)
+
+ if filter_by and id:
+ column = FILTER_MAP.get(filter_by)
+ if column is not None:
+ filter_name = None
+ 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
+ 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
+ else:
+ if not (item := db.session.query(Item).filter(Item.id == id).first()):
+ return "Invalid Type ID", 400
+ filter_name = item.description
+
+ query = query.filter(column == id)
+ else:
+ return "Invalid filter_by parameter", 400
+
+ inventory = query.all()
+
+ return render_template(
+ '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,
+ rows=[{"id": item.id, "cells": [row_fn(item) for row_fn in inventory_headers.values()]} for item in inventory],
+ entry_route = 'inventory_item'
+ )
+
+@main.route("/inventory/index")
+def inventory_index():
+ category = request.args.get('category')
+ listing = None
+
+ if category == 'user':
+ users = db.session.query(User.id, User.first_name, User.last_name).order_by(User.first_name, User.last_name).all()
+ listing = chunk_list([(user.id, f"{user.first_name or ''} {user.last_name or ''}".strip()) for user in users], 12)
+ elif category == 'location':
+ rooms = (
+ db.session.query(Room.id, Room.name, RoomFunction.description)
+ .join(RoomFunction, Room.function_id == RoomFunction.id)
+ .order_by(Room.name, RoomFunction.description)
+ .all()
+ )
+ listing = chunk_list([(room.id, f"{room.name or ''} - {room.description or ''}".strip()) for room in rooms], 12)
+ elif category == 'type':
+ types = db.session.query(Item.id, Item.description).order_by(Item.description).all()
+ 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,
+ listing=listing
+ )
+
+@main.route("/inventory_item/", methods=['GET', 'POST'])
+def inventory_item(id):
+ try:
+ id = int(id)
+ except ValueError:
+ return render_template('error.html', title="Bad ID", message="ID must be an integer", endpoint='inventory_item', endpoint_args={'id': -1})
+
+ inventory_query = db.session.query(Inventory)
+ item = eager_load_inventory_relationships(inventory_query).filter(Inventory.id == id).first()
+ brands = db.session.query(Brand).all()
+ users = eager_load_user_relationships(db.session.query(User)).all()
+ rooms = eager_load_room_relationships(db.session.query(Room)).all()
+ worklog_query = db.session.query(WorkLog).filter(WorkLog.work_item_id == id)
+ worklog = eager_load_worklog_relationships(worklog_query).all()
+ types = db.session.query(Item).all()
+ filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Work Item', 'Contact', 'Follow Up?', 'Quick Analysis?']}
+
+ if item:
+ title = f"Inventory Record - {item.identifier}"
+ else:
+ title = "Inventory Record - Not Found"
+ 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,
+ 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],
+ types=types
+ )
+
+@main.route("/inventory_item/new", methods=["GET"])
+def new_inventory_item():
+ brands = db.session.query(Brand).all()
+ users = eager_load_user_relationships(db.session.query(User)).all()
+ rooms = eager_load_room_relationships(db.session.query(Room)).all()
+ types = db.session.query(Item).all()
+
+ item = Inventory(
+ timestamp=datetime.datetime.now(),
+ condition="Unverified",
+ needed="",
+ type_id=None,
+ )
+
+ return render_template(
+ "inventory.html",
+ item=item,
+ brands=brands,
+ users=users,
+ rooms=rooms,
+ types=types,
+ worklog=[],
+ worklog_headers={},
+ worklog_rows=[]
+ )
+
+@main.route("/api/inventory", methods=["POST"])
+def create_inventory_item():
+ try:
+ data = request.get_json(force=True)
+
+ new_item = Inventory.from_dict(data)
+
+ db.session.add(new_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/", methods=["PUT"])
+def update_inventory_item(id):
+ try:
+ data = request.get_json(force=True)
+ item = db.session.query(Inventory).get(id)
+
+ if not item:
+ return jsonify({"success": False, "error": f"Inventory item with ID {id} not found."}), 404
+
+ item.timestamp = datetime.datetime.fromisoformat(data.get("timestamp")) if data.get("timestamp") else item.timestamp
+ item.condition = data.get("condition", item.condition)
+ item.needed = data.get("needed", item.needed)
+ item.type_id = data.get("type_id", item.type_id)
+ item.inventory_name = data.get("inventory_name", item.inventory_name)
+ item.serial = data.get("serial", item.serial)
+ item.model = data.get("model", item.model)
+ item.notes = data.get("notes", item.notes)
+ item.owner_id = data.get("owner_id", item.owner_id)
+ item.brand_id = data.get("brand_id", item.brand_id)
+ item.location_id = data.get("location_id", item.location_id)
+ item.barcode = data.get("barcode", item.barcode)
+ item.shared = bool(data.get("shared", item.shared))
+
+ db.session.commit()
+
+ return jsonify({"success": True, "id": item.id}), 200
+
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({"success": False, "error": str(e)}), 400
+
+@main.route("/api/inventory/", methods=["DELETE"])
+def delete_inventory_item(id):
+ try:
+ item = db.session.query(Inventory).get(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
+
diff --git a/routes/search.py b/routes/search.py
new file mode 100644
index 0000000..b87fb5b
--- /dev/null
+++ b/routes/search.py
@@ -0,0 +1,68 @@
+from flask import request, redirect, url_for, render_template
+from sqlalchemy import or_
+from sqlalchemy.orm import aliased
+
+from . import main
+from .helpers import inventory_headers, user_headers, worklog_headers
+from .. import db
+from ..models import Inventory, User, WorkLog
+from ..utils.load import eager_load_inventory_relationships, eager_load_user_relationships, eager_load_worklog_relationships
+
+@main.route("/search")
+def search():
+ query = request.args.get('q', '').strip()
+
+ if not query:
+ return redirect(url_for('main.index'))
+
+ InventoryAlias = aliased(Inventory)
+ UserAlias = aliased(User)
+
+ inventory_query = eager_load_inventory_relationships(db.session.query(Inventory).join(UserAlias, Inventory.owner)).filter(
+ or_(
+ Inventory.inventory_name.ilike(f"%{query}%"),
+ Inventory.serial.ilike(f"%{query}%"),
+ Inventory.barcode.ilike(f"%{query}%"),
+ Inventory.notes.ilike(f"%{query}%"),
+ UserAlias.first_name.ilike(f"%{query}%"),
+ UserAlias.last_name.ilike(f"%{query}%")
+ ))
+ inventory_results = inventory_query.all()
+ user_query = eager_load_user_relationships(db.session.query(User).join(UserAlias, User.supervisor)).filter(
+ or_(
+ User.first_name.ilike(f"%{query}%"),
+ User.last_name.ilike(f"%{query}%"),
+ UserAlias.first_name.ilike(f"%{query}%"),
+ UserAlias.last_name.ilike(f"%{query}%")
+ ))
+ user_results = user_query.all()
+ worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog).join(UserAlias, WorkLog.contact).join(InventoryAlias, WorkLog.work_item)).filter(
+ or_(
+ WorkLog.notes.ilike(f"%{query}%"),
+ UserAlias.first_name.ilike(f"%{query}%"),
+ UserAlias.last_name.ilike(f"%{query}%"),
+ InventoryAlias.inventory_name.ilike(f"%{query}%"),
+ InventoryAlias.serial.ilike(f"%{query}%"),
+ InventoryAlias.barcode.ilike(f"%{query}%")
+ ))
+ worklog_results = worklog_query.all()
+
+ results = {
+ 'inventory': {
+ 'results': inventory_query,
+ 'headers': inventory_headers,
+ 'rows': [{"id": item.id, "cells": [fn(item) for fn in inventory_headers.values()]} for item in inventory_results]
+ },
+ 'users': {
+ 'results': user_query,
+ 'headers': user_headers,
+ 'rows': [{"id": user.id, "cells": [fn(user) for fn in user_headers.values()]} for user in user_results]
+ },
+ 'worklog': {
+ 'results': worklog_query,
+ 'headers': worklog_headers,
+ 'rows': [{"id": log.id, "cells": [fn(log) for fn in worklog_headers.values()]} for log in worklog_results]
+ }
+ }
+
+ return render_template('search.html', title=f"Database Search ({query})" if query else "Database Search", results=results, query=query)
diff --git a/routes/settings.py b/routes/settings.py
new file mode 100644
index 0000000..f650227
--- /dev/null
+++ b/routes/settings.py
@@ -0,0 +1,115 @@
+import json
+import traceback
+
+from flask import request, flash, redirect, url_for, render_template, jsonify
+
+from . import main
+from .. import db
+from ..models import Brand, Item, Area, RoomFunction, Room
+from ..utils.load import eager_load_room_relationships
+
+@main.route('/settings', methods=['GET', 'POST'])
+def settings():
+ if request.method == 'POST':
+ print("⚠️⚠️⚠️ POST /settings reached! ⚠️⚠️⚠️")
+ form = request.form
+ print("📝 Raw form payload:", form)
+
+ try:
+ state = json.loads(form['formState'])
+ import pprint
+ print("🧠 Parsed state:")
+ pprint.pprint(state, indent=2, width=120)
+ except Exception:
+ flash("Invalid form state submitted. JSON decode failed.", "danger")
+ traceback.print_exc()
+ return redirect(url_for('main.settings'))
+
+ try:
+ with db.session.begin():
+ # Sync each table and grab temp ID maps
+ brand_map = Brand.sync_from_state(state.get("brands", []))
+ type_map = Item.sync_from_state(state.get("types", []))
+ section_map = Area.sync_from_state(state.get("sections", []))
+ function_map = RoomFunction.sync_from_state(state.get("functions", []))
+
+ # Fix up room foreign keys based on real IDs
+ submitted_rooms = []
+ for room in state.get("rooms", []):
+ room = dict(room) # shallow copy
+ sid = room.get("section_id")
+ fid = room.get("function_id")
+
+ if sid is not None:
+ sid_key = str(sid)
+ if sid_key in section_map:
+ room["section_id"] = section_map[sid_key]
+
+ if fid is not None:
+ fid_key = str(fid)
+ if fid_key in function_map:
+ room["function_id"] = function_map[fid_key]
+
+ submitted_rooms.append(room)
+
+ Room.sync_from_state(
+ submitted_rooms=submitted_rooms,
+ section_map=section_map,
+ function_map=function_map
+ )
+
+ print("✅ COMMIT executed.")
+ flash("Changes saved.", "success")
+
+ except Exception as e:
+ print("❌ COMMIT FAILED ❌")
+ traceback.print_exc()
+ flash(f"Error saving changes: {e}", "danger")
+
+ return redirect(url_for('main.settings'))
+
+ # === GET ===
+ brands = db.session.query(Brand).order_by(Brand.name).all()
+ types = db.session.query(Item).order_by(Item.description).all()
+ sections = db.session.query(Area).order_by(Area.name).all()
+ functions = db.session.query(RoomFunction).order_by(RoomFunction.description).all()
+ rooms = eager_load_room_relationships(db.session.query(Room).order_by(Room.name)).all()
+
+ return render_template('settings.html',
+ title="Settings",
+ brands=[b.serialize() for b in brands],
+ types=[{"id": t.id, "name": t.description} for t in types],
+ sections=[s.serialize() for s in sections],
+ functions=[f.serialize() for f in functions],
+ rooms=[r.serialize() for r in rooms]
+ )
+
+@main.route("/api/settings", methods=["POST"])
+def api_settings():
+ try:
+ payload = request.get_json(force=True)
+ except Exception as e:
+ return jsonify({"error": "Invalid JSON"}), 400
+
+ errors = []
+ errors += Brand.validate_state(payload.get("brands", []))
+ errors += Item.validate_state(payload.get("types", []))
+ errors += Area.validate_state(payload.get("sections", []))
+ errors += RoomFunction.validate_state(payload.get("functions", []))
+ errors += Room.validate_state(payload.get("rooms", []))
+
+ if errors:
+ return jsonify({"errors": errors}), 400
+
+ try:
+ with db.session.begin():
+ section_map = Area.sync_from_state(payload["sections"])
+ function_map = RoomFunction.sync_from_state(payload["functions"])
+ Brand.sync_from_state(payload["brands"])
+ Item.sync_from_state(payload["types"])
+ Room.sync_from_state(payload["rooms"], section_map, function_map)
+ except Exception as e:
+ db.session.rollback()
+ return jsonify({"errors": [str(e)]}), 500
+
+ return jsonify({"message": "Settings updated successfully."}), 200
diff --git a/routes/user.py b/routes/user.py
new file mode 100644
index 0000000..094f082
--- /dev/null
+++ b/routes/user.py
@@ -0,0 +1,66 @@
+from flask import render_template
+
+from . import main
+from .helpers import ACTIVE_STATUSES, user_headers, inventory_headers, worklog_headers
+from .. import db
+from ..utils.load import eager_load_user_relationships, eager_load_room_relationships, eager_load_inventory_relationships, eager_load_worklog_relationships
+from ..models import User, Room, Inventory, WorkLog
+
+@main.route("/users")
+def list_users():
+ query = eager_load_user_relationships(db.session.query(User)).order_by(User.last_name, User.first_name)
+ users = query.all()
+ 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],
+ title = "Users",
+ entry_route = 'user'
+ )
+
+@main.route("/user/")
+def user(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})
+
+ users_query = db.session.query(User).order_by(User.first_name, User.last_name)
+ users = eager_load_user_relationships(users_query).all()
+ user = next((u for u in users if u.id == id), None)
+ rooms_query = db.session.query(Room)
+ rooms = eager_load_room_relationships(rooms_query).all()
+ inventory_query = (
+ eager_load_inventory_relationships(db.session.query(Inventory))
+ .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', 'Inventory #', 'Serial #',
+ 'Bar Code #', 'Condition', 'Owner', 'Notes',
+ 'Brand', 'Model', 'Shared?', 'Location']}
+ worklog_query = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.contact_id == id)
+ worklog = worklog_query.order_by(WorkLog.start_time.desc()).all()
+ 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)"
+ else:
+ title = f"User Record - User Not Found"
+ return render_template(
+ 'error.html',
+ title=title,
+ message=f"User with id {id} not found!"
+ )
+
+ return render_template(
+ "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,
+ worklog_headers=filtered_worklog_headers,
+ worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog]
+ )
\ No newline at end of file
diff --git a/routes/worklog.py b/routes/worklog.py
new file mode 100644
index 0000000..1eb4c46
--- /dev/null
+++ b/routes/worklog.py
@@ -0,0 +1,51 @@
+from flask import request, render_template
+
+from . import main
+from .helpers import worklog_headers, worklog_form_fields
+from .. import db
+from ..models import WorkLog, User, Inventory
+from ..utils.load import eager_load_worklog_relationships, eager_load_user_relationships, eager_load_inventory_relationships
+
+@main.route("/worklog")
+def list_worklog(page=1):
+ page = request.args.get('page', default=1, type=int)
+ query = eager_load_worklog_relationships(db.session.query(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()],
+ title="Work Log",
+ entry_route='worklog_entry'
+ )
+
+@main.route("/worklog/")
+def worklog_entry(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})
+
+ log = eager_load_worklog_relationships(db.session.query(WorkLog)).filter(WorkLog.id == id).first()
+ user_query = db.session.query(User)
+ users = eager_load_user_relationships(user_query).all()
+ item_query = db.session.query(Inventory)
+ items = eager_load_inventory_relationships(item_query).all()
+
+ if log:
+ title = f'Work Log - Entry #{id}'
+ else:
+ title = "Work Log - Entry Not Found"
+ return render_template(
+ 'error.html',
+ title=title,
+ message=f"The work log with ID {id} is not found!"
+ )
+
+ return render_template(
+ "worklog.html",
+ title=title,
+ log=log,
+ users=users,
+ items=items,
+ form_fields=worklog_form_fields
+ )