Add dynamic custom field display and remove redundant admin routes

Major improvements to tenant management:
1. Dynamic field display - Product lists now automatically show all custom fields based on tenant configuration (e.g., inventory, custom attributes)
2. Fixed Jinja2 template scoping issue - Product field values now correctly display in edit forms using selectattr filter
3. Simplified URL structure - Removed redundant /tenant/admin/ routes, all product management now at /tenant/ root
4. Enhanced tenant landing page - Added delete functionality with confirmation modal, shows all custom fields except images
5. Updated all routes and redirects to use simplified paths (/tenant/add, /tenant/edit, etc.)

This consolidates the interface so /tenant/ serves as the main dashboard with full product management capabilities, while /tenant/settings handles configuration.
This commit is contained in:
2025-10-20 17:11:26 -04:00
parent 92eb45fc8c
commit f781cdf602
7 changed files with 142 additions and 103 deletions

View File

@@ -73,8 +73,9 @@ def tenant_index(tenant_id):
# Load products for this tenant # Load products for this tenant
products = load_products(tenant_id) products = load_products(tenant_id)
custom_fields = database.get_custom_ar_fields(tenant_id)
return render_template('index.html', tenant=tenant, products=products) return render_template('index.html', tenant=tenant, products=products, custom_fields=custom_fields)
@app.route('/<tenant:tenant_id>/settings') @app.route('/<tenant:tenant_id>/settings')
def tenant_settings(tenant_id): def tenant_settings(tenant_id):
@@ -292,21 +293,16 @@ def serve_barcode(tenant_id, filename):
app.logger.error(f"Barcode generation error: {str(e)}") app.logger.error(f"Barcode generation error: {str(e)}")
return jsonify({"error": "Failed to generate barcode"}), 500 return jsonify({"error": "Failed to generate barcode"}), 500
# Import the admin routes directly and register them with tenant support # Import product management routes
from routes.admin import (index as admin_index, add_product, edit_product, from routes.admin import (add_product, edit_product, delete_product,
delete_product, view_product, generate_barcode, generate_barcode, generate_barcode_page)
generate_barcode_page, manage_credentials, manage_ar_fields)
# Register admin routes with tenant prefix # Register product management routes (remove /admin/ from paths)
app.add_url_rule('/<tenant:tenant_id>/admin/', 'admin.index', admin_index) app.add_url_rule('/<tenant:tenant_id>/add', 'admin.add_product', add_product, methods=['GET', 'POST'])
app.add_url_rule('/<tenant:tenant_id>/admin/add', 'admin.add_product', add_product, methods=['GET', 'POST']) app.add_url_rule('/<tenant:tenant_id>/edit/<product_id>', 'admin.edit_product', edit_product, methods=['GET', 'POST'])
app.add_url_rule('/<tenant:tenant_id>/admin/edit/<product_id>', 'admin.edit_product', edit_product, methods=['GET', 'POST']) app.add_url_rule('/<tenant:tenant_id>/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST'])
app.add_url_rule('/<tenant:tenant_id>/admin/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST']) app.add_url_rule('/<tenant:tenant_id>/generate_barcode/<product_id>/<code_type>', 'admin.generate_barcode', generate_barcode)
app.add_url_rule('/<tenant:tenant_id>/admin/view/<product_id>', 'admin.view_product', view_product) app.add_url_rule('/<tenant:tenant_id>/generate_barcode_page/<product_id>', 'admin.generate_barcode_page', generate_barcode_page)
app.add_url_rule('/<tenant:tenant_id>/admin/generate_barcode/<product_id>/<code_type>', 'admin.generate_barcode', generate_barcode)
app.add_url_rule('/<tenant:tenant_id>/admin/generate_barcode_page/<product_id>', 'admin.generate_barcode_page', generate_barcode_page)
app.add_url_rule('/<tenant:tenant_id>/admin/credentials', 'admin.manage_credentials', manage_credentials, methods=['GET', 'POST'])
app.add_url_rule('/<tenant:tenant_id>/admin/ar_fields', 'admin.manage_ar_fields', manage_ar_fields, methods=['GET', 'POST'])
# Register API routes with tenant prefix # Register API routes with tenant prefix
from routes.api import api_index from routes.api import api_index

View File

@@ -38,7 +38,8 @@ def save_products(products):
def index(tenant_id): def index(tenant_id):
products = load_products(tenant_id) products = load_products(tenant_id)
tenant = database.get_or_create_tenant(tenant_id) tenant = database.get_or_create_tenant(tenant_id)
return render_template('admin/index.html', products=products, tenant=tenant) custom_fields = database.get_custom_ar_fields(tenant_id)
return render_template('admin/index.html', products=products, tenant=tenant, custom_fields=custom_fields)
def add_product(tenant_id): def add_product(tenant_id):
# Get custom AR fields for this tenant # Get custom AR fields for this tenant
@@ -172,7 +173,7 @@ def add_product(tenant_id):
database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type'])
flash('Product added successfully!') flash('Product added successfully!')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields) return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields)
@@ -182,7 +183,7 @@ def edit_product(tenant_id, product_id):
if product_id not in products: if product_id not in products:
flash('Product not found.') flash('Product not found.')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
if request.method == 'POST': if request.method == 'POST':
# Handle backward compatibility image upload # Handle backward compatibility image upload
@@ -295,7 +296,7 @@ def edit_product(tenant_id, product_id):
database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type']) database.save_product_image(product_id, tenant_id, img['field_name'], img['data'], img['mime_type'])
flash('Product updated successfully!') flash('Product updated successfully!')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
return render_template('admin/edit_product.html', return render_template('admin/edit_product.html',
product_id=product_id, product_id=product_id,
@@ -308,20 +309,20 @@ def delete_product(tenant_id, product_id):
if product_id not in products: if product_id not in products:
flash('Product not found.') flash('Product not found.')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
# Delete product from database (image is stored in DB) # Delete product from database (image is stored in DB)
database.delete_product(product_id, tenant_id) database.delete_product(product_id, tenant_id)
flash('Product deleted successfully!') flash('Product deleted successfully!')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
def view_product(tenant_id, product_id): def view_product(tenant_id, product_id):
products = load_products(tenant_id) products = load_products(tenant_id)
if product_id not in products: if product_id not in products:
flash('Product not found.') flash('Product not found.')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
return render_template('admin/view_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) return render_template('admin/view_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id)
@@ -349,7 +350,7 @@ def generate_barcode_page(tenant_id, product_id):
if product_id not in products: if product_id not in products:
flash('Product not found.') flash('Product not found.')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
# Extract product data # Extract product data
product_data = {} product_data = {}
@@ -388,7 +389,7 @@ def manage_credentials(tenant_id):
if username and password: if username and password:
database.update_tenant_credentials(tenant_id, username, password) database.update_tenant_credentials(tenant_id, username, password)
flash('Credentials updated successfully!') flash('Credentials updated successfully!')
return redirect(url_for('admin.index', tenant_id=tenant_id)) return redirect(f'/{tenant_id}/')
else: else:
flash('Username and password are required.') flash('Username and password are required.')

View File

@@ -10,7 +10,7 @@
<h2>Add New Product</h2> <h2>Add New Product</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('admin.add_product', tenant_id=tenant_id) }}" method="POST" enctype="multipart/form-data"> <form action="/{{ tenant_id }}/add" method="POST" enctype="multipart/form-data">
<div class="mb-3"> <div class="mb-3">
<label for="product_id" class="form-label">Product ID *</label> <label for="product_id" class="form-label">Product ID *</label>
<div class="input-group"> <div class="input-group">
@@ -67,7 +67,7 @@
{% endif %} {% endif %}
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a> <a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Add Product</button> <button type="submit" class="btn btn-primary">Add Product</button>
</div> </div>
</form> </form>

View File

@@ -10,7 +10,7 @@
<h2>Edit Product</h2> <h2>Edit Product</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('admin.edit_product', tenant_id=tenant_id, product_id=product_id) }}" method="POST" enctype="multipart/form-data"> <form action="/{{ tenant_id }}/edit/{{ product_id }}" method="POST" enctype="multipart/form-data">
<div class="mb-3"> <div class="mb-3">
<label for="product_id" class="form-label">Product ID</label> <label for="product_id" class="form-label">Product ID</label>
<input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled> <input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled>
@@ -22,12 +22,9 @@
<div class="mb-3"> <div class="mb-3">
<label for="field_{{ field.fieldName }}" class="form-label">{{ field.label }}</label> <label for="field_{{ field.fieldName }}" class="form-label">{{ field.label }}</label>
{% set current_value = '' %} {# Find current value using selectattr filter #}
{% for prod_field in product %} {% set matching_fields = product|selectattr('fieldName', 'equalto', field.fieldName)|list %}
{% if prod_field.fieldName == field.fieldName %} {% set current_value = matching_fields[0].value if matching_fields else '' %}
{% set current_value = prod_field.value %}
{% endif %}
{% endfor %}
{% if field.fieldType == 'IMAGE_URI' %} {% if field.fieldType == 'IMAGE_URI' %}
<input type="file" class="form-control" id="image_{{ field.fieldName }}" name="image_{{ field.fieldName }}" accept="image/*" onchange="previewImage(this, 'preview_new_{{ field.fieldName }}')"> <input type="file" class="form-control" id="image_{{ field.fieldName }}" name="image_{{ field.fieldName }}" accept="image/*" onchange="previewImage(this, 'preview_new_{{ field.fieldName }}')">
@@ -93,7 +90,7 @@
{% endif %} {% endif %}
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a> <a href="/{{ tenant_id }}/" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Update Product</button> <button type="submit" class="btn btn-primary">Update Product</button>
</div> </div>
</form> </form>

View File

@@ -8,7 +8,7 @@
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h2>Generate Barcodes for Product: {{ product_id }}</h2> <h2>Generate Barcodes for Product: {{ product_id }}</h2>
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-outline-secondary">Back to Products</a> <a href="/{{ tenant_id }}/" class="btn btn-outline-secondary">Back to Products</a>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %} {% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}

View File

@@ -22,40 +22,38 @@
<table class="table table-striped table-bordered align-middle"> <table class="table table-striped table-bordered align-middle">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th style="width: 15%">Product ID</th> <th>Product ID</th>
<th style="width: 25%">Name</th> {% for custom_field in custom_fields %}
<th style="width: 10%">Price</th> {% if custom_field.fieldName != '_id' %}
<th style="width: 20%">Image</th> <th>{{ custom_field.label }}</th>
<th style="width: 30%">Actions</th> {% endif %}
{% endfor %}
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for product_id, product_data in products.items() %} {% for product_id, product_data in products.items() %}
<tr> <tr>
<td>{{ product_id }}</td> <td>{{ product_id }}</td>
<td> {% for custom_field in custom_fields %}
{% for field in product_data %} {% if custom_field.fieldName != '_id' %}
{% if field.fieldName == '_name' %} <td>
{{ field.value }} {% for field in product_data %}
{% endif %} {% if field.fieldName == custom_field.fieldName %}
{% endfor %} {% if custom_field.fieldType == 'IMAGE_URI' %}
</td> {% if field.value %}
<td> <div class="product-image-container">
{% for field in product_data %} <img src="{{ field.value }}" alt="{{ custom_field.label }}" class="product-image">
{% if field.fieldName == '_price' %} </div>
{{ field.value }} {% endif %}
{% endif %} {% else %}
{% endfor %} {{ field.value }}
</td> {% endif %}
<td> {% endif %}
{% for field in product_data %} {% endfor %}
{% if field.fieldName == '_image' %} </td>
<div class="product-image-container"> {% endif %}
<img src="{{ field.value }}" alt="Product image" class="product-image"> {% endfor %}
</div>
{% endif %}
{% endfor %}
</td>
<td> <td>
<div class="d-flex justify-content-center gap-2"> <div class="d-flex justify-content-center gap-2">
<a href="{{ url_for('admin.edit_product', tenant_id=tenant.id, product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a> <a href="{{ url_for('admin.edit_product', tenant_id=tenant.id, product_id=product_id) }}" class="btn btn-sm btn-outline-primary">Edit</a>

View File

@@ -18,7 +18,7 @@
<div class="mt-4"> <div class="mt-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2>Products</h2> <h2>Products</h2>
<a href="/{{ tenant.id }}/admin/add" class="btn btn-primary"> <a href="/{{ tenant.id }}/add" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Product <i class="bi bi-plus-circle"></i> Add New Product
</a> </a>
</div> </div>
@@ -29,8 +29,11 @@
<thead> <thead>
<tr> <tr>
<th>Product ID</th> <th>Product ID</th>
<th>Name</th> {% for custom_field in custom_fields %}
<th>Price</th> {% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %}
<th>{{ custom_field.label }}</th>
{% endif %}
{% endfor %}
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -38,38 +41,35 @@
{% for product_id, product_data in products.items() %} {% for product_id, product_data in products.items() %}
<tr> <tr>
<td>{{ product_id }}</td> <td>{{ product_id }}</td>
{% for custom_field in custom_fields %}
{% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %}
<td>
{% set matching_fields = product_data|selectattr('fieldName', 'equalto', custom_field.fieldName)|list %}
{{ matching_fields[0].value if matching_fields else '' }}
</td>
{% endif %}
{% endfor %}
<td> <td>
{% for field in product_data %} <a href="/{{ tenant.id }}/edit/{{ product_id }}" class="btn btn-sm btn-outline-primary">
{% if field.fieldName == '_name' %}
{{ field.value }}
{% endif %}
{% endfor %}
</td>
<td>
{% for field in product_data %}
{% if field.fieldName == '_price' %}
{{ field.value }}
{% endif %}
{% endfor %}
</td>
<td>
<a href="/{{ tenant.id }}/admin/edit/{{ product_id }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Edit <i class="bi bi-pencil"></i> Edit
</a> </a>
<a href="/{{ tenant.id }}/admin/generate_barcode_page/{{ product_id }}" class="btn btn-sm btn-outline-success"> <a href="/{{ tenant.id }}/generate_barcode_page/{{ product_id }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-upc-scan"></i> Barcodes <i class="bi bi-upc-scan"></i> Barcodes
</a> </a>
<button type="button" class="btn btn-sm btn-outline-danger"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-product-id="{{ product_id }}"
data-product-name="{% set matching_fields = product_data|selectattr('fieldName', 'equalto', '_name')|list %}{{ matching_fields[0].value if matching_fields else product_id }}">
<i class="bi bi-trash"></i> Delete
</button>
<form id="delete-form-{{ product_id }}" action="/{{ tenant.id }}/delete/{{ product_id }}" method="POST" style="display: none;">
</form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-3">
<a href="/{{ tenant.id }}/admin/" class="btn btn-secondary">
<i class="bi bi-list-task"></i> Manage All Products
</a>
</div>
{% else %} {% else %}
<div class="alert alert-info"> <div class="alert alert-info">
<i class="bi bi-info-circle"></i> No products available yet. Click "Add New Product" to get started. <i class="bi bi-info-circle"></i> No products available yet. Click "Add New Product" to get started.
@@ -89,4 +89,51 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">Confirm Deletion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete the product <strong id="delete-product-name"></strong>?</p>
<p class="text-danger"><i class="bi bi-exclamation-triangle-fill"></i> This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirm-delete-btn">Delete Product</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Delete confirmation modal functionality
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
const button = event.relatedTarget;
// Extract product info from data attributes
const productId = button.getAttribute('data-product-id');
const productName = button.getAttribute('data-product-name');
// Update modal content
const modalProductName = document.getElementById('delete-product-name');
modalProductName.textContent = productName + " (ID: " + productId + ")";
// Setup the confirm button action
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
confirmDeleteBtn.onclick = function() {
document.getElementById('delete-form-' + productId).submit();
};
});
}
</script>
{% endblock %} {% endblock %}