Add title field to User model and update related views for user management

This commit is contained in:
Yaro Kasear 2025-07-25 13:21:05 -05:00
parent fd148eb484
commit f89b825ef0
6 changed files with 94 additions and 14 deletions

View file

@ -19,6 +19,7 @@ class User(db.Model, ImageAttachable):
active: Mapped[Optional[bool]] = mapped_column(Boolean, server_default=text('((0))'))
last_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
first_name: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True)
title: Mapped[Optional[str]] = mapped_column(Unicode(255), nullable=True, default=None)
location_id: Mapped[Optional[int]] = mapped_column(ForeignKey("rooms.id"), nullable=True, index=True)
supervisor_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), nullable=True, index=True)
image_id: Mapped[Optional[int]] = mapped_column(ForeignKey('images.id', ondelete='SET NULL'), nullable=True, index=True)
@ -35,10 +36,12 @@ class User(db.Model, ImageAttachable):
return f"{self.first_name or ''} {self.last_name or ''}".strip()
def __init__(self, first_name: Optional[str] = None, last_name: Optional[str] = None,
location_id: Optional[int] = None, supervisor_id: Optional[int] = None,
staff: Optional[bool] = False, active: Optional[bool] = False):
title: Optional[str] = None,location_id: Optional[int] = None,
supervisor_id: Optional[int] = None, staff: Optional[bool] = False,
active: Optional[bool] = False):
self.first_name = first_name
self.last_name = last_name
self.title = title
self.location_id = location_id
self.supervisor_id = supervisor_id
self.staff = staff
@ -53,6 +56,7 @@ class User(db.Model, ImageAttachable):
'id': self.id,
'first_name': self.first_name,
'last_name': self.last_name,
'title': self.title,
'location_id': self.location_id,
'supervisor_id': self.supervisor_id,
'staff': self.staff,
@ -66,6 +70,7 @@ class User(db.Model, ImageAttachable):
active=bool(data.get("active", False)),
last_name=data.get("last_name"),
first_name=data.get("first_name"),
title=data.get("title"),
location_id=data.get("location_id"),
supervisor_id=data.get("supervisor_id")
)

View file

@ -52,6 +52,7 @@ FILTER_MAP = {
user_headers = {
"Last Name": lambda i: {"text": i.last_name},
"First Name": lambda i: {"text": i.first_name},
"Title": lambda i: {"text": i.title},
"Supervisor": lambda i: {"text": i.supervisor.identifier, "url": url_for("main.user", id=i.supervisor.id)} if i.supervisor else {"text": None},
"Location": lambda i: {"text": i.location.identifier} if i.location else {"text": None},
"Staff?": lambda i: {"text": i.staff, "type": "bool", "html": checked_box if i.staff else unchecked_box},

View file

@ -14,7 +14,7 @@ def search():
if not query:
return redirect(url_for('main.index'))
InventoryAlias = aliased(Inventory)
UserAlias = aliased(User)
@ -25,15 +25,18 @@ def search():
Inventory.barcode.ilike(f"%{query}%"),
Inventory.notes.ilike(f"%{query}%"),
UserAlias.first_name.ilike(f"%{query}%"),
UserAlias.last_name.ilike(f"%{query}%")
UserAlias.last_name.ilike(f"%{query}%"),
UserAlias.title.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}%"),
User.title.ilike(f"%{query}%"),
UserAlias.first_name.ilike(f"%{query}%"),
UserAlias.last_name.ilike(f"%{query}%")
UserAlias.last_name.ilike(f"%{query}%"),
UserAlias.title.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(
@ -41,26 +44,27 @@ def search():
WorkLog.notes.ilike(f"%{query}%"),
UserAlias.first_name.ilike(f"%{query}%"),
UserAlias.last_name.ilike(f"%{query}%"),
UserAlias.title.ilike(f"%{query}%"),
InventoryAlias.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,
'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,
'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,
'results': worklog_query,
'headers': worklog_headers,
'rows': [{"id": log.id, "cells": [fn(log) for fn in worklog_headers.values()]} for log in worklog_results]
}
}

View file

@ -70,6 +70,32 @@ def user(id):
worklog_rows=[{"id": log.id, "cells": [fn(log) for fn in filtered_worklog_headers.values()]} for log in worklog]
)
@main.route("/user/<id>/org")
def user_org(id):
user = eager_load_user_relationships(db.session.query(User).filter(User.id == id)).first()
if not user:
return render_template('error.html', title='User Not Found', message=f'User with ID {id} not found.')
current_user = user
org_chart = []
while current_user:
subordinates = (
eager_load_user_relationships(
db.session.query(User).filter(User.supervisor_id == current_user.id)
).all()
)
org_chart.insert(0, {
"user": current_user,
"subordinates": [subordinate for subordinate in subordinates if subordinate.active and subordinate.staff]
})
current_user = current_user.supervisor
return render_template(
"user_org.html",
user=user,
org_chart=org_chart
)
@main.route("/user/new", methods=["GET"])
def new_user():
rooms = eager_load_room_relationships(db.session.query(Room)).all()
@ -116,6 +142,7 @@ def update_user(id):
user.active = bool(data.get("active", user.active))
user.last_name = data.get("last_name", user.last_name)
user.first_name = data.get("first_name", user.first_name)
user.title = data.get("title", user.title)
user.location_id = data.get("location_id", user.location_id)
user.supervisor_id = data.get("supervisor_id", user.supervisor_id)
@ -155,6 +182,7 @@ def get_user_csv():
"active",
"last_name",
"first_name",
"title",
"location",
"supervisor"
]

View file

@ -12,6 +12,7 @@
active: document.querySelector("input[name='activeCheck']").checked,
last_name: document.querySelector("input[name='lastName']").value,
first_name: document.querySelector("input[name='firstName']").value,
title: document.querySelector("input[name='title']").value,
supervisor_id: parseInt(document.querySelector("input[name='supervisor']").value) || null,
location_id: parseInt(document.querySelector("input[name='location']").value) || null
};
@ -84,15 +85,20 @@
<div class="container">
<form action="POST">
<div class="row">
<div class="col-6">
<div class="col">
<label for="lastName" class="form-label">Last Name</label>
<input type="text" class="form-control" id="lastName" name="lastName" placeholder="Doe" value="{{ user.last_name if user.last_name else '' }}"{% if not user.active %} disabled readonly{% endif %}>
</div>
<div class="col-6">
<div class="col">
<label for="firstName" class="form-label">First Name</label>
<input type="text" class="form-control" id="firstName" name="firstName" placeholder="John" value="{{ user.first_name if user.first_name else '' }}"{% if not user.active %} disabled readonly{% endif %}>
</div>
<div class="col">
<label for="title" class="form-label">Title</label>
<input type="text" class="form-control" id="title" name="title" placeholder="President" value="{{ user.title if user.title else '' }}"{% if not user.active %} disabled readonly{% endif %}>
</div>
</div>
<div class="row mt-2">

View file

@ -0,0 +1,36 @@
{% extends 'layout.html' %}
{% block content %}
{% for layer in org_chart %}
{# Only show the top-level user on the first iteration #}
{% if loop.first %}
<div class="row align-items-center justify-content-center mb-3">
<div class="column text-center fw-bold">
{{ layer.user.identifier }}
</div>
</div>
{% endif %}
{# Render this layer's subordinates #}
{% if layer['subordinates'] %}
<div class="row align-items-center justify-content-center mb-3 border">
{% set current_index = loop.index0 %}
{% set next_user = org_chart[current_index + 1].user if current_index + 1 < org_chart|length else None %}
{% for subordinate in layer.subordinates %}
<div class="col text-center {% if next_user and subordinate.id == next_user.id %}fw-bold{% endif %}">
{% if subordinate == user %}
{{ subordinate.identifier }}
{% else %}
<a class="link-success link-underline-opacity-0"
href="{{ url_for('main.user_org', id=subordinate.id) }}">
{{ subordinate.identifier }}
</a>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
{% endblock %}