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:
26
src/app.py
26
src/app.py
@@ -73,8 +73,9 @@ def tenant_index(tenant_id):
|
||||
|
||||
# Load products for this tenant
|
||||
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')
|
||||
def tenant_settings(tenant_id):
|
||||
@@ -292,21 +293,16 @@ def serve_barcode(tenant_id, filename):
|
||||
app.logger.error(f"Barcode generation error: {str(e)}")
|
||||
return jsonify({"error": "Failed to generate barcode"}), 500
|
||||
|
||||
# Import the admin routes directly and register them with tenant support
|
||||
from routes.admin import (index as admin_index, add_product, edit_product,
|
||||
delete_product, view_product, generate_barcode,
|
||||
generate_barcode_page, manage_credentials, manage_ar_fields)
|
||||
# Import product management routes
|
||||
from routes.admin import (add_product, edit_product, delete_product,
|
||||
generate_barcode, generate_barcode_page)
|
||||
|
||||
# Register admin routes with tenant prefix
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/', 'admin.index', admin_index)
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/add', 'admin.add_product', add_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>/admin/delete/<product_id>', 'admin.delete_product', delete_product, methods=['POST'])
|
||||
app.add_url_rule('/<tenant:tenant_id>/admin/view/<product_id>', 'admin.view_product', view_product)
|
||||
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 product management routes (remove /admin/ from paths)
|
||||
app.add_url_rule('/<tenant:tenant_id>/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>/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>/generate_barcode_page/<product_id>', 'admin.generate_barcode_page', generate_barcode_page)
|
||||
|
||||
# Register API routes with tenant prefix
|
||||
from routes.api import api_index
|
||||
|
||||
@@ -38,7 +38,8 @@ def save_products(products):
|
||||
def index(tenant_id):
|
||||
products = load_products(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):
|
||||
# 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'])
|
||||
|
||||
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)
|
||||
|
||||
@@ -182,7 +183,7 @@ def edit_product(tenant_id, product_id):
|
||||
|
||||
if product_id not in products:
|
||||
flash('Product not found.')
|
||||
return redirect(url_for('admin.index', tenant_id=tenant_id))
|
||||
return redirect(f'/{tenant_id}/')
|
||||
|
||||
if request.method == 'POST':
|
||||
# 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'])
|
||||
|
||||
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',
|
||||
product_id=product_id,
|
||||
@@ -308,20 +309,20 @@ def delete_product(tenant_id, product_id):
|
||||
|
||||
if product_id not in products:
|
||||
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)
|
||||
database.delete_product(product_id, tenant_id)
|
||||
|
||||
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):
|
||||
products = load_products(tenant_id)
|
||||
|
||||
if product_id not in products:
|
||||
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)
|
||||
|
||||
@@ -349,7 +350,7 @@ def generate_barcode_page(tenant_id, product_id):
|
||||
|
||||
if product_id not in products:
|
||||
flash('Product not found.')
|
||||
return redirect(url_for('admin.index', tenant_id=tenant_id))
|
||||
return redirect(f'/{tenant_id}/')
|
||||
|
||||
# Extract product data
|
||||
product_data = {}
|
||||
@@ -388,7 +389,7 @@ def manage_credentials(tenant_id):
|
||||
if username and password:
|
||||
database.update_tenant_credentials(tenant_id, username, password)
|
||||
flash('Credentials updated successfully!')
|
||||
return redirect(url_for('admin.index', tenant_id=tenant_id))
|
||||
return redirect(f'/{tenant_id}/')
|
||||
else:
|
||||
flash('Username and password are required.')
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2>Add New Product</h2>
|
||||
</div>
|
||||
<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">
|
||||
<label for="product_id" class="form-label">Product ID *</label>
|
||||
<div class="input-group">
|
||||
@@ -67,7 +67,7 @@
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<h2>Edit Product</h2>
|
||||
</div>
|
||||
<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">
|
||||
<label for="product_id" class="form-label">Product ID</label>
|
||||
<input type="text" class="form-control" id="product_id" value="{{ product_id }}" disabled>
|
||||
@@ -22,12 +22,9 @@
|
||||
<div class="mb-3">
|
||||
<label for="field_{{ field.fieldName }}" class="form-label">{{ field.label }}</label>
|
||||
|
||||
{% set current_value = '' %}
|
||||
{% for prod_field in product %}
|
||||
{% if prod_field.fieldName == field.fieldName %}
|
||||
{% set current_value = prod_field.value %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Find current value using selectattr filter #}
|
||||
{% set matching_fields = product|selectattr('fieldName', 'equalto', field.fieldName)|list %}
|
||||
{% set current_value = matching_fields[0].value if matching_fields else '' %}
|
||||
|
||||
{% 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 }}')">
|
||||
@@ -93,7 +90,7 @@
|
||||
{% endif %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<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 class="card-body">
|
||||
{% if not dependencies.qrcode or not dependencies.barcode or not dependencies.pillow %}
|
||||
|
||||
@@ -22,40 +22,38 @@
|
||||
<table class="table table-striped table-bordered align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 15%">Product ID</th>
|
||||
<th style="width: 25%">Name</th>
|
||||
<th style="width: 10%">Price</th>
|
||||
<th style="width: 20%">Image</th>
|
||||
<th style="width: 30%">Actions</th>
|
||||
<th>Product ID</th>
|
||||
{% for custom_field in custom_fields %}
|
||||
{% if custom_field.fieldName != '_id' %}
|
||||
<th>{{ custom_field.label }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product_id, product_data in products.items() %}
|
||||
<tr>
|
||||
<td>{{ product_id }}</td>
|
||||
{% for custom_field in custom_fields %}
|
||||
{% if custom_field.fieldName != '_id' %}
|
||||
<td>
|
||||
{% for field in product_data %}
|
||||
{% 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>
|
||||
{% for field in product_data %}
|
||||
{% if field.fieldName == '_image' %}
|
||||
{% if field.fieldName == custom_field.fieldName %}
|
||||
{% if custom_field.fieldType == 'IMAGE_URI' %}
|
||||
{% if field.value %}
|
||||
<div class="product-image-container">
|
||||
<img src="{{ field.value }}" alt="Product image" class="product-image">
|
||||
<img src="{{ field.value }}" alt="{{ custom_field.label }}" class="product-image">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ field.value }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<td>
|
||||
<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>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
@@ -29,8 +29,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product ID</th>
|
||||
<th>Name</th>
|
||||
<th>Price</th>
|
||||
{% for custom_field in custom_fields %}
|
||||
{% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %}
|
||||
<th>{{ custom_field.label }}</th>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -38,38 +41,35 @@
|
||||
{% for product_id, product_data in products.items() %}
|
||||
<tr>
|
||||
<td>{{ product_id }}</td>
|
||||
{% for custom_field in custom_fields %}
|
||||
{% if custom_field.fieldName != '_id' and custom_field.fieldType != 'IMAGE_URI' %}
|
||||
<td>
|
||||
{% for field in product_data %}
|
||||
{% if field.fieldName == '_name' %}
|
||||
{{ field.value }}
|
||||
{% 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 %}
|
||||
{% 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">
|
||||
<a href="/{{ tenant.id }}/edit/{{ product_id }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</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
|
||||
</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>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
<div class="alert alert-info">
|
||||
<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>
|
||||
|
||||
<!-- 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 %}
|
||||
|
||||
Reference in New Issue
Block a user