I lost track of all these changes. Congratulations me.

This commit is contained in:
Yaro Kasear 2025-10-31 16:31:23 -05:00
parent 8481a40553
commit 4ef4d5e23f
6 changed files with 170 additions and 68 deletions

View file

@ -81,7 +81,11 @@ def _fields_for_model(model: str):
"title", "title",
"active", "active",
"staff", "staff",
"supervisor.id" "supervisor.id",
"inventory.label",
"inventory.brand.name",
"inventory.model",
"inventory.device_type.description"
] ]
fields_spec = [ fields_spec = [
{"name": "label", "row": "label", "label": "", "type": "display", {"name": "label", "row": "label", "label": "", "type": "display",
@ -106,6 +110,7 @@ def _fields_for_model(model: str):
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}}, "row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
{"name": "staff", "label": "Staff Member", "label_attrs": {"class": "form-check-label"}, {"name": "staff", "label": "Staff Member", "label_attrs": {"class": "form-check-label"},
"row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}}, "row": "checkboxes", "attrs": {"class": "form-check-input"}, "wrap": {"class": "form-check"}},
{"name": "inventory", "label": "Inventory", "type": "template", "row": "inventory", "template": "user_inventory.html"},
] ]
layout = [ layout = [
{"name": "label", "order": 0, "attrs": {"class": "row align-items-center"}}, {"name": "label", "order": 0, "attrs": {"class": "row align-items-center"}},
@ -113,6 +118,7 @@ def _fields_for_model(model: str):
{"name": "details", "order": 20, "attrs": {"class": "row mt-2"}}, {"name": "details", "order": 20, "attrs": {"class": "row mt-2"}},
{"name": "checkboxes", "order": 30, "parent": "details", {"name": "checkboxes", "order": 30, "parent": "details",
"attrs": {"class": "col d-flex flex-column justify-content-end"}}, "attrs": {"class": "col d-flex flex-column justify-content-end"}},
{"name": "inventory", "order": 40},
] ]
elif model == "worklog": elif model == "worklog":
@ -127,7 +133,7 @@ def _fields_for_model(model: str):
"updates.id", "updates.id",
"updates.content", "updates.content",
"updates.timestamp", "updates.timestamp",
"updates.is_deleted", "updates.is_deleted"
] ]
fields_spec = [ fields_spec = [
{"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}", {"name": "id", "label": "", "type": "display", "label_spec": "Work Item #{id}",

View file

@ -16,6 +16,7 @@ bp_index = Blueprint("index", __name__)
def init_index_routes(app): def init_index_routes(app):
@bp_index.get("/") @bp_index.get("/")
def index(): def index():
# 1. work log stuff (leave it)
work_log_service = crudkit.crud.get_service(WorkLog) work_log_service = crudkit.crud.get_service(WorkLog)
work_logs = work_log_service.list({ work_logs = work_log_service.list({
"complete__ne": 1, "complete__ne": 1,
@ -33,36 +34,69 @@ def init_index_routes(app):
{"field": "work_item.label", "label": "Work Item", {"field": "work_item.label", "label": "Work Item",
"link": {"endpoint": "entry.entry", "params": {"id": "{work_item.id}", "model": "inventory"}}} "link": {"endpoint": "entry.entry", "params": {"id": "{work_item.id}", "model": "inventory"}}}
] ]
logs = render_table(work_logs, columns=columns, opts={"object_class": "worklog"}) logs = render_table(work_logs, columns=columns, opts={"object_class": "worklog"})
# 2. get device types with targets
device_type_service = crudkit.crud.get_service(DeviceType) device_type_service = crudkit.crud.get_service(DeviceType)
device_types = device_type_service.list({ dt_rows = device_type_service.list({
'limit': 0, 'limit': 0,
'target__gt': 0, 'target__gt': 0,
'fields': [ 'fields': [
'description', 'description',
'target', 'target'
'condition.category'
], ],
"sort": "description",
}) })
device_types = [d.as_dict() for d in device_types]
dt_ids = [d['id'] for d in device_types]
dt_filter = {'$or': [
{'device_type_id': d} for d in dt_ids
],
'condition.category': 'Available'}
# turn into df
device_types = pd.DataFrame([d.as_dict() for d in dt_rows])
# if nobody has targets, just show empty table
if device_types.empty:
empty_df = pd.DataFrame(columns=['id', 'description', 'target', 'actual', 'needed'])
return render_template("index.html", logs=logs, needed_inventory=empty_df)
# 3. now we can safely collect ids from the DF
dt_ids = device_types['id'].tolist()
# 4. build inventory filter
dt_filter = {
'$or': [{'device_type_id': d} for d in dt_ids],
# drop this if you decided to ignore condition
'condition.category': 'Available'
}
# 5. fetch inventory
inventory_service = crudkit.crud.get_service(Inventory) inventory_service = crudkit.crud.get_service(Inventory)
needed_inventory = inventory_service.list({ inv_rows = inventory_service.list({
'limit': 0, 'limit': 0,
**dt_filter, **dt_filter,
'fields': ['device_type.description'] 'fields': ['device_type.description'],
'sort': 'device_type.description',
}) })
needed_inventory = pd.DataFrame([i.as_dict() for i in needed_inventory]) inventory_df = pd.DataFrame([i.as_dict() for i in inv_rows])
needed_inventory = pd.pivot_table(needed_inventory, columns='device_type.description', aggfunc='size')
return render_template("index.html", logs=logs, device_types=device_types, needed_inventory=needed_inventory) # if there is no inventory for these device types, actual = 0
if inventory_df.empty:
device_types['actual'] = 0
device_types['needed'] = device_types['target']
return render_template("index.html", logs=logs, needed_inventory=device_types)
# 6. aggregate counts
inv_counts = (
inventory_df['device_type.description']
.value_counts()
.rename('actual')
.reset_index()
.rename(columns={'device_type.description': 'description'})
)
# 7. merge
merged = device_types.merge(inv_counts, on='description', how='left')
merged['actual'] = merged['actual'].fillna(0).astype(int)
merged['needed'] = (merged['target'] - merged['actual']).clip(lower=0)
return render_template("index.html", logs=logs, needed_inventory=merged)
@bp_index.get("/LICENSE") @bp_index.get("/LICENSE")
def license(): def license():

View file

@ -53,7 +53,6 @@ def init_settings_routes(app):
], ],
}) })
statuses = render_table(statuses, opts={"object_class": 'status'}) statuses = render_table(statuses, opts={"object_class": 'status'})
print([t.as_dict() for t in device_types])
return render_template("settings.html", brands=brands, device_types=device_types, areas=areas, functions=functions, rooms=rooms, statuses=statuses) return render_template("settings.html", brands=brands, device_types=device_types, areas=areas, functions=functions, rooms=rooms, statuses=statuses)

View file

@ -11,14 +11,38 @@
</div> </div>
<div class="col"> <div class="col">
<p class="display-6 text-center">Supply Status</p> <p class="display-6 text-center">Supply Status</p>
{% for d in device_types %} <div class="table-responsive">
<p> <table class="table table-sm table-bordered table-striped table-hover">
{{ d['description'] }}: {{ d['target'] }} needed {% if not needed_inventory.empty %}
</p> <thead>
{% endfor %} <tr>
<pre class="border border-black bg-warning-subtle p-2"> <th>Device</th>
{{ needed_inventory }} <th>Target</th>
</pre> <th>On Hand</th>
<th>Needed</th>
</tr>
</thead>
<tbody>
{% for row in needed_inventory.itertuples() %}
<tr class="{{ 'table-warning' if row.needed else '' }}"
onclick="location.href='{{ url_for('listing.show_list', model='inventory', device_type_id__eq=row.id) }}&condition.category=Available'"
style="cursor: pointer;">
<td>{{ row.description }}</td>
<td>{{ row.target }}</td>
<td>{{ row.actual }}</td>
<td class="{{ 'fw-bold' if row.needed else '' }}">{{ row.needed }}</td>
</tr>
{% endfor %}
</tbody>
{% else %}
<thead>
<tr>
<th colspan="4" class="text-center">No data.</th>
</tr>
</thead>
{% endif %}
</table>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,48 +1,54 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block style %} {% block style %}
thead.sticky-top th { thead.sticky-top th {
z-index: 2; z-index: 2;
} }
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<h1 class="display-4 text-center mb-3">Inventory Summary</h1> <h1 class="display-4 text-center mb-3">Inventory Summary</h1>
<div class="table-responsive mx-5 overflow-y-auto border" style="max-height: 70vh;"> <div class="table-responsive mx-5 overflow-y-auto border" style="max-height: 70vh;">
<table class="table table-sm table-striped table-hover table-bordered align-middle mb-0"> <table class="table table-sm table-striped table-hover table-bordered align-middle mb-0">
<thead> <thead>
<tr class="position-sticky top-0 bg-body border"> <tr class="position-sticky top-0 bg-body border">
<th class="text-nowrap position-sticky top-0 bg-body border">Device Type</th> <th class="text-nowrap position-sticky top-0 bg-body border">Device Type</th>
{% for col in col_headers %} {% for col in col_headers %}
{% if col.href %} {% if col.href %}
<th class="text-end position-sticky top-0 bg-body border"><a class="link-dark link-underline link-underline-opacity-0" href="{{ col.href }}">{{ col.label }}</a></th> <th class="text-end position-sticky top-0 bg-body border"><a
{% else %} class="link-dark link-underline link-underline-opacity-0" href="{{ col.href }}">{{ col.label }}</a></th>
<th class="text-end position-sticky top-0 bg-body border">{{ col.label }}</th> {% else %}
{% endif %} <th class="text-end position-sticky top-0 bg-body border">{{ col.label }}</th>
{% endfor %} {% endif %}
</tr>
</thead>
<tbody>
{% for row in table_rows %}
{% set need_more = (row['cells'][-2]['value'] | int > 0) %}
<tr class="{% if need_more %}table-warning{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
<th class="text-nowrap{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
{% if row.href %}
<a class="link-dark link-underline link-underline-opacity-0" href="{{ row.href }}">{{ row.label }}</a>
{% else %}
{{ row.label }}
{% endif %}
</th>
{% for cell in row.cells %}
{% if cell.href %}
<td class="text-end{% if need_more and loop.index == (row.cells|length - 1) %} fw-bold{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}"><a class="link-dark link-underline link-underline-opacity-0" href="{{ cell.href }}">{{ cell.value }}</a></td>
{% else %}
<td class="text-end{% if need_more and loop.index == (row.cells|length - 1) %} fw-bold{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">{{ cell.value }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %} {% endfor %}
</tbody> </tr>
</table> </thead>
</div> <tbody>
{% endblock %} {% for row in table_rows %}
{% set need_more = (row['cells'][-2]['value'] | int > 0) %}
<tr
class="{% if need_more %}table-warning{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
<th class="text-nowrap{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
{% if row.href %}
<a class="link-dark link-underline link-underline-opacity-0" href="{{ row.href }}">{{ row.label }}</a>
{% else %}
{{ row.label }}
{% endif %}
</th>
{% for cell in row.cells %}
{% if cell.href %}
<td
class="text-end{% if need_more and loop.index == (row.cells|length - 1) %} fw-bold{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
<a class="link-dark link-underline link-underline-opacity-0" href="{{ cell.href }}">{{ cell.value }}</a></td>
{% else %}
<td
class="text-end{% if need_more and loop.index == (row.cells|length - 1) %} fw-bold{% endif %}{% if loop.index == table_rows|length %} position-sticky bottom-0 border{% endif %}">
{{ cell.value }}</td>
{% endif %}
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View file

@ -0,0 +1,33 @@
<label class="form-label mt-2">Assigned Inventory</label>
{% set inv = field['template_ctx']['values']['inventory'] %}
<div class="table-responsive">
<table class="table table-sm table-bordered table-striped table-hover">
{% if inv %}
<thead>
<tr>
<th>Device</th>
<th>Brand</th>
<th>Model</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{% for i in inv %}
<tr style="cursor: pointer;" onclick="location.href='{{ url_for('entry.entry', model='inventory', id=i.id) }}'">
<td>{{ i.label }}</td>
<td>{{ i['brand.name'] }}</td>
<td>{{ i.model }}</td>
<td>{{ i['device_type.description'] }}</td>
</tr>
{% endfor %}
</tbody>
{% else %}
<thead>
<tr>
<th colspan="4" class="text-center">No data.</th>
</tr>
</thead>
{% endif %}
</table>
</div>