commit 189f73b7c23dcb10cb252ab4f4a11d7709433663 Author: Yaro Kasear Date: Wed Jun 11 09:10:41 2025 -0500 Initial commit. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..199ca67 --- /dev/null +++ b/__init__.py @@ -0,0 +1,37 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from urllib.parse import quote_plus +import logging + +db = SQLAlchemy() + +logger = logging.getLogger('sqlalchemy.engine') +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) +logger.addHandler(handler) + +def create_app(): + app = Flask(__name__) + + params = quote_plus( + "DRIVER=ODBC Driver 17 for SQL Server;" + "SERVER=NDVASQLCR01;" + "DATABASE=conradTest;" + "Trusted_Connection=yes;" + ) + + app.config['SQLALCHEMY_DATABASE_URI'] = f"mssql+pyodbc:///?odbc_connect={params}" + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + + db.init_app(app) + + from .routes import main + app.register_blueprint(main) + + @app.route("/") + def index(): + return "Hello, you've reached the terrible but functional root of the site." + + + return app diff --git a/__pycache__/__init__.cpython-313.pyc b/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..c48d94d Binary files /dev/null and b/__pycache__/__init__.cpython-313.pyc differ diff --git a/__pycache__/app.cpython-313.pyc b/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..d51648a Binary files /dev/null and b/__pycache__/app.cpython-313.pyc differ diff --git a/__pycache__/routes.cpython-313.pyc b/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000..5802dc8 Binary files /dev/null and b/__pycache__/routes.cpython-313.pyc differ diff --git a/__pycache__/utils.cpython-313.pyc b/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000..5c2e3fb Binary files /dev/null and b/__pycache__/utils.cpython-313.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..9b63deb --- /dev/null +++ b/app.py @@ -0,0 +1,6 @@ +from . import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(debug=True) \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..774536f --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from .. import db # If you're in an actual module +from .areas import Area +from .brands import Brand +from .items import Item +from .inventory import Inventory +from .room_functions import RoomFunction +from .users import User +from .work_log import WorkLog +from .rooms import Room \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..37b766d Binary files /dev/null and b/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/models/__pycache__/areas.cpython-313.pyc b/models/__pycache__/areas.cpython-313.pyc new file mode 100644 index 0000000..fd697d2 Binary files /dev/null and b/models/__pycache__/areas.cpython-313.pyc differ diff --git a/models/__pycache__/brands.cpython-313.pyc b/models/__pycache__/brands.cpython-313.pyc new file mode 100644 index 0000000..00aabd6 Binary files /dev/null and b/models/__pycache__/brands.cpython-313.pyc differ diff --git a/models/__pycache__/inventory.cpython-313.pyc b/models/__pycache__/inventory.cpython-313.pyc new file mode 100644 index 0000000..9805a68 Binary files /dev/null and b/models/__pycache__/inventory.cpython-313.pyc differ diff --git a/models/__pycache__/items.cpython-313.pyc b/models/__pycache__/items.cpython-313.pyc new file mode 100644 index 0000000..d9fb40c Binary files /dev/null and b/models/__pycache__/items.cpython-313.pyc differ diff --git a/models/__pycache__/room_functions.cpython-313.pyc b/models/__pycache__/room_functions.cpython-313.pyc new file mode 100644 index 0000000..c7b816f Binary files /dev/null and b/models/__pycache__/room_functions.cpython-313.pyc differ diff --git a/models/__pycache__/rooms.cpython-313.pyc b/models/__pycache__/rooms.cpython-313.pyc new file mode 100644 index 0000000..ffd9659 Binary files /dev/null and b/models/__pycache__/rooms.cpython-313.pyc differ diff --git a/models/__pycache__/users.cpython-313.pyc b/models/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000..0812292 Binary files /dev/null and b/models/__pycache__/users.cpython-313.pyc differ diff --git a/models/__pycache__/work_log.cpython-313.pyc b/models/__pycache__/work_log.cpython-313.pyc new file mode 100644 index 0000000..11854dc Binary files /dev/null and b/models/__pycache__/work_log.cpython-313.pyc differ diff --git a/models/areas.py b/models/areas.py new file mode 100644 index 0000000..a3ebfe9 --- /dev/null +++ b/models/areas.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .rooms import Room + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Area(db.Model): + __tablename__ = 'Areas' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Area", Unicode(255), nullable=True) + + rooms: Mapped[List['Room']] = relationship('Room', back_populates='area') + + def __repr__(self): + return f"" diff --git a/models/brands.py b/models/brands.py new file mode 100644 index 0000000..168894e --- /dev/null +++ b/models/brands.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Brand(db.Model): + __tablename__ = 'Brands' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Brand", Unicode(255), nullable=True) + + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='brand') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/inventory.py b/models/inventory.py new file mode 100644 index 0000000..ffca1d5 --- /dev/null +++ b/models/inventory.py @@ -0,0 +1,73 @@ +from typing import Any, List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .brands import Brand + from .items import Item + from .users import User + from .work_log import WorkLog + from .rooms import Room + +from sqlalchemy import Boolean, ForeignKeyConstraint, ForeignKey, Identity, Index, Integer, PrimaryKeyConstraint, String, Unicode, text +from sqlalchemy.dialects.mssql import DATETIME2, MONEY +from sqlalchemy.orm import Mapped, mapped_column, relationship +import datetime + +from . import db + +class Inventory(db.Model): + __tablename__ = 'Inventory' + __table_args__ = ( + Index('Inventory$Bar Code', 'Bar Code'), + ) + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + timestamp: Mapped[datetime.datetime] = mapped_column('Date Entered', DATETIME2) + condition: Mapped[str] = mapped_column('Working Condition', Unicode(255)) + needed: Mapped[str] = mapped_column("Needed", Unicode(255)) + type_id: Mapped[int] = mapped_column('Item Type', Integer, ForeignKey("Items.ID")) + inventory_name: Mapped[Optional[str]] = mapped_column('Inventory #', Unicode(255)) + serial: Mapped[Optional[str]] = mapped_column('Serial #', Unicode(255)) + model: Mapped[Optional[str]] = mapped_column('Model #', Unicode(255)) + notes: Mapped[Optional[str]] = mapped_column('Notes', Unicode(255)) + owner_id = mapped_column('Owner', Integer, ForeignKey('Users.ID')) + brand_id: Mapped[Optional[int]] = mapped_column("Brand", Integer, ForeignKey("Brands.ID")) + # Photo: Mapped[Optional[str]] = mapped_column(String(8000)) Will be replacing with something that actually works. + location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("Rooms.ID")) + barcode: Mapped[Optional[str]] = mapped_column('Bar Code', Unicode(255)) + shared: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))')) + + location: Mapped[Optional['Room']] = relationship('Room', back_populates='inventory') + owner = relationship('User', back_populates='inventory') + brand: Mapped[Optional['Brand']] = relationship('Brand', back_populates='inventory') + item: Mapped['Item'] = relationship('Item', back_populates='inventory') + work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='work_item') + + def __repr__(self): + parts = [f"id={self.id}"] + + if self.inventory_name: + parts.append(f"name={repr(self.inventory_name)}") + + if self.item: + parts.append(f"item={repr(self.item.description)}") + + if self.notes: + parts.append(f"notes={repr(self.notes)}") + + if self.owner: + parts.append(f"owner={repr(self.owner.full_name)}") + + if self.location: + parts.append(f"location={repr(self.location.full_name)}") + + return f"" + + @property + def identifier(self) -> str: + if self.inventory_name: + return f"Name: {self.inventory_name}" + elif self.barcode: + return f"Bar: {self.barcode}" + elif self.serial: + return f"Serial: {self.serial}" + else: + return f"ID: {self.id}" diff --git a/models/items.py b/models/items.py new file mode 100644 index 0000000..0e0ae0e --- /dev/null +++ b/models/items.py @@ -0,0 +1,20 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Item(db.Model): + __tablename__ = 'Items' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + description: Mapped[Optional[str]] = mapped_column("Description", Unicode(255), nullable=True) + category: Mapped[Optional[str]] = mapped_column("Category", Unicode(255), nullable=True) + + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='item') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/room_functions.py b/models/room_functions.py new file mode 100644 index 0000000..8683174 --- /dev/null +++ b/models/room_functions.py @@ -0,0 +1,19 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .rooms import Room + +from sqlalchemy import Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class RoomFunction(db.Model): + __tablename__ = 'Room Functions' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + description: Mapped[Optional[str]] = mapped_column("Function", Unicode(255), nullable=True) + + rooms: Mapped[List['Room']] = relationship('Room', back_populates='room_function') + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/models/rooms.py b/models/rooms.py new file mode 100644 index 0000000..9323535 --- /dev/null +++ b/models/rooms.py @@ -0,0 +1,34 @@ +from typing import Optional, TYPE_CHECKING, List +if TYPE_CHECKING: + from .areas import Area + from .room_functions import RoomFunction + from .inventory import Inventory + from .users import User + +from sqlalchemy import ForeignKey, Identity, Integer, Unicode +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class Room(db.Model): + __tablename__ = 'Rooms' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + name: Mapped[Optional[str]] = mapped_column("Room", Unicode(255), nullable=True) + area_id: Mapped[Optional[int]] = mapped_column("Area", Integer, ForeignKey("Areas.ID")) + function_id: Mapped[Optional[int]] = mapped_column("Function", Integer, ForeignKey("Room Functions.ID")) + + area: Mapped[Optional['Area']] = relationship('Area', back_populates='rooms') + room_function: Mapped[Optional['RoomFunction']] = relationship('RoomFunction', back_populates='rooms') + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='location') + users: Mapped[List['User']] = relationship('User', back_populates='location') + + def __repr__(self): + return f"" + + @property + def full_name(self): + name = self.name or "" + func = self.room_function.description if self.room_function else "" + return f"{name} - {func}".strip(" -") + diff --git a/models/users.py b/models/users.py new file mode 100644 index 0000000..7df3c17 --- /dev/null +++ b/models/users.py @@ -0,0 +1,36 @@ +from typing import List, Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + from .rooms import Room + from .work_log import WorkLog + +from sqlalchemy import Boolean, ForeignKey, Identity, Integer, Unicode, text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from . import db + +class User(db.Model): + __tablename__ = 'Users' + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + staff: Mapped[Optional[bool]] = mapped_column("Staff", Boolean, server_default=text('((0))')) + active: Mapped[Optional[bool]] = mapped_column("Active", Boolean, server_default=text('((0))')) + last_name: Mapped[Optional[str]] = mapped_column("Last", Unicode(255), nullable=True) + first_name: Mapped[Optional[str]] = mapped_column("First", Unicode(255), nullable=True) + location_id: Mapped[Optional[str]] = mapped_column(ForeignKey("Rooms.ID")) + supervisor_id: Mapped[Optional[int]] = mapped_column("Supervisor", Integer, ForeignKey("Users.ID")) + + supervisor: Mapped[Optional['User']] = relationship('User', remote_side='User.id', back_populates='subordinates') + subordinates: Mapped[List['User']] = relationship('User', back_populates='supervisor') + + work_logs: Mapped[List['WorkLog']] = relationship('WorkLog', back_populates='contact') + location: Mapped[Optional['Room']] = relationship('Room', back_populates='users') + inventory: Mapped[List['Inventory']] = relationship('Inventory', back_populates='owner') + + @property + def full_name(self) -> str: + return f"{self.first_name or ''} {self.last_name or ''}".strip() + + def __repr__(self): + return f"" diff --git a/models/work_log.py b/models/work_log.py new file mode 100644 index 0000000..fbea696 --- /dev/null +++ b/models/work_log.py @@ -0,0 +1,35 @@ +from typing import Optional, TYPE_CHECKING +if TYPE_CHECKING: + from .inventory import Inventory + from .inventory import User + +from sqlalchemy import Boolean, ForeignKeyConstraint, Identity, Integer, ForeignKey, Unicode, text +from sqlalchemy.dialects.mssql import DATETIME2 +from sqlalchemy.orm import Mapped, mapped_column, relationship +import datetime + +from . import db + +class WorkLog(db.Model): + __tablename__ = 'Work Log' + __table_args__ = ( + ForeignKeyConstraint(['Work Item'], ['Inventory.ID'], name='Work Log$InventoryWork Log'), + ) + + id: Mapped[int] = mapped_column("ID", Integer, Identity(start=1, increment=1), primary_key=True) + start_time: Mapped[Optional[datetime.datetime]] = mapped_column('Start Timestamp', DATETIME2) + end_time: Mapped[Optional[datetime.datetime]] = mapped_column('End Timestamp', DATETIME2) + notes: Mapped[Optional[str]] = mapped_column('Description & Notes', Unicode()) + complete: Mapped[Optional[bool]] = mapped_column("Complete", Boolean, server_default=text('((0))')) + followup: Mapped[Optional[bool]] = mapped_column('Needs Follow-Up', Boolean, server_default=text('((0))')) + contact_id: Mapped[Optional[int]] = mapped_column('Point of Contact', Integer, ForeignKey("Users.ID")) + analysis: Mapped[Optional[bool]] = mapped_column('Needs Quick Analysis', Boolean, server_default=text('((0))')) + work_item_id: Mapped[Optional[int]] = mapped_column("Work Item", Integer, ForeignKey("Inventory.ID")) + + work_item: Mapped[Optional['Inventory']] = relationship('Inventory', back_populates='work_logs') + contact: Mapped[Optional['User']] = relationship('User', back_populates='work_logs') + + def __repr__(self): + return f"" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b8d2de2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +flask_sqlalchemy +pyodbc diff --git a/routes.py b/routes.py new file mode 100644 index 0000000..f181b50 --- /dev/null +++ b/routes.py @@ -0,0 +1,236 @@ +from flask import Blueprint, render_template, url_for, request +from .models import Area, Brand, Item, Inventory, RoomFunction, User, WorkLog, Room +import html +from sqlalchemy.orm import joinedload +from typing import Callable, Any, List +from . import db +from .utils import eager_load_user_relationships, eager_load_inventory_relationships, eager_load_room_relationships, eager_load_worklog_relationships + +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}, + "Quick Analysis?": lambda i: {"text": i.analysis, "type": "bool", "html": checked_box if i.analysis else unchecked_box}, +} + +def make_paginated_data( + query, + page: int, + per_page=15 +): + model = query.column_descriptions[0]['entity'] + items = ( + query.order_by(model.id) + .limit(per_page) + .offset((page - 1) * per_page) + .all() + ) + has_next = len(items) == per_page + has_prev = page > 1 + total_items = query.count() + total_pages = (total_items + per_page - 1) // per_page + return { + "items": items, + "has_next": has_next, + "has_prev": has_prev, + "total_pages": total_pages, + "page": page + } + +def render_paginated_table( + query, + page: int, + title: str, + headers: dict, + entry_route: str, + row_fn: Callable[[Any], List[dict]], + endpoint: str, + per_page=15 +): + data = make_paginated_data(query, page, per_page) + return render_template( + "table.html", + header=headers.keys(), + rows=[{"id": item.id, "cells": row_fn(item)} for item in data['items']], + title=title, + has_next=data['has_next'], + has_prev=data['has_prev'], + page=page, + endpoint=endpoint, + total_pages=data['total_pages'], + headers=headers, + entry_route=entry_route + ) + +@main.route("/") +def index(): + return render_template("index.html", title="Inventory Manager") + +def link(text, endpoint, **values): + return {"text": text, "url": url_for(endpoint, **values)} + +@main.route("/inventory") +def list_inventory(): + page = request.args.get('page', default=1, type=int) + query = eager_load_inventory_relationships(db.session.query(Inventory)).order_by(Inventory.inventory_name, Inventory.barcode, Inventory.serial) + return render_paginated_table( + query=query, + page=page, + title="Inventory", + headers=inventory_headers, + row_fn=lambda i: [fn(i) for fn in inventory_headers.values()], + endpoint="main.list_inventory", + entry_route="inventory_item" + ) + +@main.route("/inventory_item/") +def inventory_item(id): + worklog_page = request.args.get("worklog_page", default=1, type=int) + 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_pagination = make_paginated_data(worklog_query, worklog_page, 5) + filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Work Item', 'Contact', 'Follow Up?', 'Quick Analysis?']} + worklog = worklog_pagination['items'] + + if item: + title = f"Inventory Record - {item.identifier}" + else: + title = "Inventory Record - Not Found" + + return render_template("inventory.html", title=title, item=item, + brands=brands, users=users, rooms=rooms, + worklog=worklog, + worklog_pagination=worklog_pagination, + worklog_page=worklog_page, + 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("/users") +def list_users(): + page = request.args.get('page', default=1, type=int) + query = eager_load_user_relationships(db.session.query(User)) + return render_paginated_table( + query=query, + page=page, + title="Users", + headers=user_headers, + row_fn=lambda i: [fn(i) for fn in user_headers.values()], + endpoint="main.list_users", + entry_route="user" + ) + +@main.route("/user/") +def user(id): + asset_page = request.args.get("asset_page", default=1, type=int) + worklog_page = request.args.get("worklog_page", default=1, type=int) + users_query = db.session.query(User) + 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) + .filter(Inventory.condition.in_(ACTIVE_STATUSES)) + ) + inventory_pagination = make_paginated_data(inventory_query, asset_page, 10) + inventory = inventory_pagination['items'] + 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_pagination = make_paginated_data(worklog_query, worklog_page, 10) + worklog = worklog_pagination['items'] + filtered_worklog_headers = {k: v for k, v in worklog_headers.items() if k not in ['Contact', 'Follow Up?', 'Quick Analysis?']} + return render_template( + "user.html", + title=(f"User Record - {user.full_name}" if user.active else f"User Record - {user.full_name} (Inactive)") if user else "User Record - Record Not Found", + 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], + inventory_pagination=inventory_pagination, + asset_page=asset_page, + 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], + worklog_pagination=worklog_pagination, + worklog_page=worklog_page + ) + +@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_paginated_table( + query=query, + page=page, + title="Work Log", + headers=worklog_headers, + row_fn=lambda i: [fn(i) for fn in worklog_headers.values()], + endpoint="main.list_worklog", + entry_route="worklog_entry" + ) + +@main.route("/worklog/") +def worklog_entry(id): + 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() + return render_template("worklog.html", title=f"Work Log #{id}", log=log, users=users, items=items) \ No newline at end of file diff --git a/templates/_table_fragment.html b/templates/_table_fragment.html new file mode 100644 index 0000000..7b400a8 --- /dev/null +++ b/templates/_table_fragment.html @@ -0,0 +1,75 @@ +{% macro render_table(headers, rows, entry_route=None, title=None) %} +
+ + {% if title %} + + {% endif %} + + + {% for h in headers %} + + {% endfor %} + + + + {% for row in rows %} + + {% for cell in row.cells %} + + {% endfor %} + + {% endfor %} + +
{{ title }}
{{ h }}
+ {% if cell.type == 'bool' %} + {{ cell.html | safe }} + {% elif cell.url %} + {{ cell.text }} + {% else %} + {{ cell.text or '-' }} + {% endif %} +
+
+{% endmacro %} + +{% macro render_pagination(endpoint, page, has_prev, has_next, total_pages, page_variable='page', extra_args={}) %} +{% set prev_args = extra_args.copy() %} +{% set next_args = extra_args.copy() %} +{% set first_args = extra_args.copy() %} +{% set last_args = extra_args.copy() %} +{% set _ = prev_args.update({page_variable: page - 1}) %} +{% set _ = next_args.update({page_variable: page + 1}) %} +{% set _ = first_args.update({page_variable: 1}) %} +{% set _ = last_args.update({page_variable: total_pages}) %} + +
+ + +
+
Page {{ page }} of {{ total_pages }}
+
+ {% for number in range(page - 2, page + 3) if number > 0 and number <= total_pages %} {% set + args=extra_args.copy() %} {% set _=args.update({page_variable: number}) %} + {{ number }} + + {% endfor %} +
+
+ +
+ Next > + Last » +
+
+{% endmacro %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9f22500 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,21 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +

Table of Contents

+
+
+ Inventory +
+
+ Users +
+
+ Work Log +
+
+{% endblock %} diff --git a/templates/inventory.html b/templates/inventory.html new file mode 100644 index 0000000..ce13946 --- /dev/null +++ b/templates/inventory.html @@ -0,0 +1,127 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + {% for brand in brands %} + + {% endfor %} + +
+
+ + +
+
+ + +
+
+
+
+ + + + + {% for user in users %} + + {% endfor %} + +
+
+ + + + + {% for room in rooms %} + + {% endfor %} + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+ {% if worklog %} +
+ {{ tables.render_table(worklog_headers, worklog_rows, 'worklog_entry', 'Work Log') }} + {% if worklog_pagination['total_pages'] > 1 %} + {{ tables.render_pagination( + page=worklog_pagination['page'], + has_prev=worklog_pagination['has_prev'], + has_next=worklog_pagination['has_next'], + total_pages=worklog_pagination['total_pages'], + endpoint='main.inventory_item', + page_variable='worklog_page', + extra_args={'id': item.id, 'worklog_page': worklog_page} + ) }} + {% endif %} +
+ {% endif %} +
+
+
+{% endblock %} diff --git a/templates/layout.html b/templates/layout.html new file mode 100644 index 0000000..981c2b0 --- /dev/null +++ b/templates/layout.html @@ -0,0 +1,53 @@ + + + + + + + + {% block title %}Inventory{% endblock %} + + + + + +
+

{{ title }}

+
+
{% block content %}{% endblock %}
+ + + + + + \ No newline at end of file diff --git a/templates/table.html b/templates/table.html new file mode 100644 index 0000000..3774e75 --- /dev/null +++ b/templates/table.html @@ -0,0 +1,20 @@ + +{% extends "layout.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + + +{% import "_table_fragment.html" as tables %} +{{ tables.render_table(header, rows, entry_route) }} +{{ tables.render_pagination(endpoint, page, has_prev, has_next, total_pages) }} +{% endblock %} \ No newline at end of file diff --git a/templates/user.html b/templates/user.html new file mode 100644 index 0000000..f6c5ebb --- /dev/null +++ b/templates/user.html @@ -0,0 +1,130 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + + +
+
+
+
+ + +
+ +
+ + +
+
+ +
+
+ + + + + {% for supervisor in users %} + + {% endfor %} + +
+ +
+ + + + + {% for location in rooms %} + + {% endfor %} + +
+
+
+
+ + +
+
+ + +
+
+
+
+ {% if inventory_rows %} +
+
+ {{ tables.render_table(inventory_headers, inventory_rows, 'inventory_item', title='Assets') }} +
+
+ {% endif %} + {% if worklog_rows %} +
+
+ {{ tables.render_table(worklog_headers, worklog_rows, 'worklog_entry', title='Work Done') }} +
+
+ {% endif %} +
+
+ {% if inventory_pagination['total_pages'] > 1 %} +
+ {{ tables.render_pagination( + page=inventory_pagination['page'], + has_prev=inventory_pagination['has_prev'], + has_next=inventory_pagination['has_next'], + total_pages=inventory_pagination['total_pages'], + endpoint='main.user', + page_variable='asset_page', + extra_args={'id': user.id, 'worklog_page': worklog_page} + ) }} +
+ {% endif %} + {% if worklog_pagination['total_pages'] > 1 %} +
+ {{ tables.render_pagination( + page=worklog_pagination['page'], + has_prev=worklog_pagination['has_prev'], + has_next=worklog_pagination['has_next'], + total_pages=worklog_pagination['total_pages'], + endpoint='main.user', + page_variable='worklog_page', + extra_args={'id': user.id, 'worklog_page': worklog_page} + ) }} +
+ {% endif %} +
+
+{% endblock %} + +{% block script %} +document.getElementById('supervisor').addEventListener('input', function() { + const input = this.value; + const options = document.querySelectorAll('#supervisorList option'); + let foundId = ''; + options.forEach(option => { + if (option.value === input) { + foundId = option.dataset.id; + } + }) + document.getElementById('supervisorId').value = foundId; +}); +{% endblock %} \ No newline at end of file diff --git a/templates/worklog.html b/templates/worklog.html new file mode 100644 index 0000000..e8376f5 --- /dev/null +++ b/templates/worklog.html @@ -0,0 +1,60 @@ + +{% extends "layout.html" %} + +{% import "_table_fragment.html" as tables %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..91f5720 --- /dev/null +++ b/utils.py @@ -0,0 +1,30 @@ +from sqlalchemy.orm import joinedload +from .models import User, Room, Inventory, WorkLog + +def eager_load_user_relationships(query): + return query.options( + joinedload(User.supervisor), + joinedload(User.location).joinedload(Room.room_function) + ) + +def eager_load_inventory_relationships(query): + return query.options( + joinedload(Inventory.owner), + joinedload(Inventory.brand), + joinedload(Inventory.item), + joinedload(Inventory.location).joinedload(Room.room_function) + ) + +def eager_load_room_relationships(query): + return query.options( + joinedload(Room.area), + joinedload(Room.room_function), + joinedload(Room.inventory), + joinedload(Room.users) + ) + +def eager_load_worklog_relationships(query): + return query.options( + joinedload(WorkLog.contact), + joinedload(WorkLog.work_item) + )