fixing field edits and adding custom fileds

This commit is contained in:
2025-06-19 15:46:22 -04:00
parent ac07f5b7c8
commit 000c054734
8 changed files with 792 additions and 180 deletions

View File

@@ -76,12 +76,8 @@ def login(tenant_id):
@app.route('/<tenant:tenant_id>/arcontentfields', methods=['GET']) @app.route('/<tenant:tenant_id>/arcontentfields', methods=['GET'])
def get_ar_content_fields(tenant_id): def get_ar_content_fields(tenant_id):
# Return a fixed set of attributes (tenant_id could be used for custom fields in the future) # Get custom fields defined for this tenant
fields = [ fields = database.get_custom_ar_fields(tenant_id)
{"fieldName": "_id", "label": "Item ID", "editable": "false", "fieldType": "TEXT"},
{"fieldName": "_price", "label": "Sale Price", "editable": "true", "fieldType": "TEXT"},
{"fieldName": "_image", "label": "Image", "editable": "false", "fieldType": "IMAGE_URI"}
]
return jsonify(fields), 200 return jsonify(fields), 200
@app.route('/<tenant:tenant_id>/arinfo', methods=['GET']) @app.route('/<tenant:tenant_id>/arinfo', methods=['GET'])
@@ -89,31 +85,39 @@ def get_ar_info(tenant_id):
barcode = request.args.get('barcode') barcode = request.args.get('barcode')
products = load_products(tenant_id) products = load_products(tenant_id)
# Helper function to convert relative image paths to absolute URLs # Get custom AR fields for this tenant
def make_absolute_urls(product_fields): custom_fields = database.get_custom_ar_fields(tenant_id)
# Create absolute URL for image fields custom_field_names = [f['fieldName'] for f in custom_fields]
# Helper function to convert relative image paths to absolute URLs and filter fields
def filter_and_process_fields(product_fields):
# Filter to only include fields defined in custom AR fields
filtered_fields = []
for field in product_fields: for field in product_fields:
if field['fieldName'] in custom_field_names:
# Create absolute URL for image fields
if field['fieldName'] == '_image' and field['value']: if field['fieldName'] == '_image' and field['value']:
# If it's already an absolute URL, leave it as is # If it's already an absolute URL, leave it as is
if not field['value'].startswith('http'): if not field['value'].startswith('http'):
# Build absolute URL using request host with tenant # Build absolute URL using request host with tenant
field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}" field['value'] = f"{request.url_root.rstrip('/')}/{tenant_id}{field['value']}"
return product_fields filtered_fields.append(field)
return filtered_fields
# If barcode is provided, return specific product # If barcode is provided, return specific product
if barcode: if barcode:
if barcode in products: if barcode in products:
product_data = make_absolute_urls(products[barcode]) product_data = filter_and_process_fields(products[barcode])
response = jsonify(product_data) response = jsonify(product_data)
response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Origin'] = '*'
return response, 200 return response, 200
return jsonify({"error": "Product not found"}), 404 return jsonify({"error": "Product not found"}), 404
# Return all products if no barcode specified # Return all products if no barcode specified
# Convert all products to have absolute URLs # Convert all products to have absolute URLs and filter fields
all_products = {} all_products = {}
for product_id, fields in products.items(): for product_id, fields in products.items():
all_products[product_id] = make_absolute_urls(fields) all_products[product_id] = filter_and_process_fields(fields)
response = jsonify(all_products) response = jsonify(all_products)
response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Origin'] = '*'
return response, 200 return response, 200
@@ -123,7 +127,26 @@ def serve_image(tenant_id, filename):
# Log the request for debugging # Log the request for debugging
app.logger.info(f"Image requested for tenant {tenant_id}: {filename}") app.logger.info(f"Image requested for tenant {tenant_id}: {filename}")
# Extract product ID from filename (e.g., "123456.jpg" -> "123456") # Check if filename contains field name (e.g., "123456_thumbnail.jpg")
base_name = os.path.splitext(filename)[0]
if '_' in base_name:
# Split to get product_id and field_name
parts = base_name.split('_', 1)
product_id = parts[0]
field_name = '_' + parts[1] if len(parts) > 1 else '_image'
# Try to get field-specific image
image_data = database.get_product_image_by_field(product_id, tenant_id, field_name)
if image_data:
image_bytes, mime_type = image_data
response = Response(image_bytes, mimetype=mime_type)
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Cache-Control'] = 'public, max-age=3600'
app.logger.info(f"Serving field-specific image: {product_id}/{field_name} ({len(image_bytes)} bytes)")
return response
# Try standard image lookup (backward compatibility)
product_id = os.path.splitext(filename)[0] product_id = os.path.splitext(filename)[0]
# Get image from database for this tenant # Get image from database for this tenant
@@ -203,7 +226,7 @@ def serve_barcode(tenant_id, filename):
# Import the admin routes directly and register them with tenant support # Import the admin routes directly and register them with tenant support
from routes.admin import (index as admin_index, add_product, edit_product, from routes.admin import (index as admin_index, add_product, edit_product,
delete_product, view_product, generate_barcode, delete_product, view_product, generate_barcode,
generate_barcode_page, manage_credentials) generate_barcode_page, manage_credentials, manage_ar_fields)
# Register admin routes with tenant prefix # 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/', 'admin.index', admin_index)
@@ -214,6 +237,7 @@ app.add_url_rule('/<tenant:tenant_id>/admin/view/<product_id>', 'admin.view_prod
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/<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/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/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

@@ -75,6 +75,38 @@ def init_database():
) )
''') ''')
# Create custom_ar_fields table for tenant-specific AR field definitions
cursor.execute('''
CREATE TABLE IF NOT EXISTS custom_ar_fields (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id TEXT NOT NULL,
field_name TEXT NOT NULL,
label TEXT NOT NULL,
field_type TEXT NOT NULL,
editable TEXT DEFAULT 'true',
display_order INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
UNIQUE(tenant_id, field_name)
)
''')
# Create product_images table for storing multiple images per product
cursor.execute('''
CREATE TABLE IF NOT EXISTS product_images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
product_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
field_name TEXT NOT NULL,
image_data BLOB,
image_mime_type TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id, tenant_id) REFERENCES products(id, tenant_id) ON DELETE CASCADE,
UNIQUE(product_id, tenant_id, field_name)
)
''')
conn.commit() conn.commit()
def migrate_from_json(): def migrate_from_json():
@@ -340,6 +372,9 @@ def get_or_create_tenant(tenant_id: str, username: str = None, password: str = N
''', (tenant_id, tenant_id.title(), default_username, default_password)) ''', (tenant_id, tenant_id.title(), default_username, default_password))
conn.commit() conn.commit()
# Initialize default AR fields for the new tenant
init_default_ar_fields(tenant_id)
return { return {
'id': tenant_id, 'id': tenant_id,
'name': tenant_id.title(), 'name': tenant_id.title(),
@@ -401,3 +436,122 @@ def cleanup_reserved_tenants():
conn.commit() conn.commit()
return deleted_count return deleted_count
def get_custom_ar_fields(tenant_id: str) -> List[Dict[str, Any]]:
"""Get custom AR fields for a tenant"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT id, field_name, label, field_type, editable, display_order
FROM custom_ar_fields
WHERE tenant_id = ?
ORDER BY display_order, field_name
''', (tenant_id,))
fields = []
for row in cursor.fetchall():
fields.append({
'id': row['id'],
'fieldName': row['field_name'],
'label': row['label'],
'fieldType': row['field_type'],
'editable': row['editable'],
'displayOrder': row['display_order']
})
return fields
def save_custom_ar_field(tenant_id: str, field_data: Dict[str, Any]) -> int:
"""Save or update a custom AR field"""
with get_db() as conn:
cursor = conn.cursor()
if 'id' in field_data:
# Update existing field
cursor.execute('''
UPDATE custom_ar_fields
SET field_name = ?, label = ?, field_type = ?, editable = ?,
display_order = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND tenant_id = ?
''', (
field_data['fieldName'],
field_data['label'],
field_data['fieldType'],
field_data.get('editable', 'true'),
field_data.get('displayOrder', 0),
field_data['id'],
tenant_id
))
conn.commit()
return field_data['id']
else:
# Insert new field
cursor.execute('''
INSERT INTO custom_ar_fields
(tenant_id, field_name, label, field_type, editable, display_order)
VALUES (?, ?, ?, ?, ?, ?)
''', (
tenant_id,
field_data['fieldName'],
field_data['label'],
field_data['fieldType'],
field_data.get('editable', 'true'),
field_data.get('displayOrder', 0)
))
conn.commit()
return cursor.lastrowid
def delete_custom_ar_field(tenant_id: str, field_id: int):
"""Delete a custom AR field"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
DELETE FROM custom_ar_fields
WHERE id = ? AND tenant_id = ?
''', (field_id, tenant_id))
conn.commit()
def init_default_ar_fields(tenant_id: str):
"""Initialize default AR fields for a new tenant"""
default_fields = [
{"fieldName": "_id", "label": "Item ID", "fieldType": "TEXT", "editable": "false", "displayOrder": 1},
{"fieldName": "_price", "label": "Sale Price", "fieldType": "TEXT", "editable": "true", "displayOrder": 2},
{"fieldName": "_image", "label": "Image", "fieldType": "IMAGE_URI", "editable": "false", "displayOrder": 3}
]
for field in default_fields:
save_custom_ar_field(tenant_id, field)
def save_product_image(product_id: str, tenant_id: str, field_name: str, image_data: bytes, mime_type: str):
"""Save an image for a specific field of a product"""
with get_db() as conn:
cursor = conn.cursor()
# Delete existing image for this field if any
cursor.execute('''
DELETE FROM product_images
WHERE product_id = ? AND tenant_id = ? AND field_name = ?
''', (product_id, tenant_id, field_name))
# Insert new image
cursor.execute('''
INSERT INTO product_images (product_id, tenant_id, field_name, image_data, image_mime_type)
VALUES (?, ?, ?, ?, ?)
''', (product_id, tenant_id, field_name, image_data, mime_type))
conn.commit()
def get_product_image_by_field(product_id: str, tenant_id: str, field_name: str) -> Optional[tuple[bytes, str]]:
"""Get image data for a specific field of a product"""
with get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT image_data, image_mime_type
FROM product_images
WHERE product_id = ? AND tenant_id = ? AND field_name = ?
''', (product_id, tenant_id, field_name))
row = cursor.fetchone()
if row and row['image_data']:
return row['image_data'], row['image_mime_type']
return None

View File

@@ -41,95 +41,196 @@ def index(tenant_id):
return render_template('admin/index.html', products=products, tenant=tenant) return render_template('admin/index.html', products=products, tenant=tenant)
def add_product(tenant_id): def add_product(tenant_id):
# Get custom AR fields for this tenant
custom_fields = database.get_custom_ar_fields(tenant_id)
if request.method == 'POST': if request.method == 'POST':
# Get form data # Get form data
product_id = request.form.get('product_id') product_id = request.form.get('product_id')
name = request.form.get('name')
price = request.form.get('price')
# Basic validation # Basic validation
if not product_id or not name or not price: if not product_id:
flash('Product ID, Name, and Price are required fields.') flash('Product ID is required.')
return render_template('admin/add_product.html', tenant_id=tenant_id) return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields)
# Check if product ID already exists # Check if product ID already exists
products = load_products(tenant_id) products = load_products(tenant_id)
if product_id in products: if product_id in products:
flash('Product ID already exists.') flash('Product ID already exists.')
return render_template('admin/add_product.html', tenant_id=tenant_id) return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields)
# Handle image upload # Handle backward compatibility image upload
image_data = None image_data = None
image_mime_type = None image_mime_type = None
image_path = f"/images/{product_id}.png" # Default path for display
if 'image' in request.files: # Create product data structure based on custom fields
file = request.files['image'] product_data = []
# Always include _id field
product_data.append({
"fieldName": "_id",
"label": "Item ID",
"value": product_id,
"editable": "false",
"fieldType": "TEXT"
})
# Process images after product is saved
images_to_save = []
# Add other fields based on custom configuration
for field in custom_fields:
field_name = field['fieldName']
if field_name == '_id':
continue # Already added
elif field['fieldType'] == 'IMAGE_URI':
# Check if image was uploaded for this field
image_field_name = f'image_{field_name}'
if image_field_name in request.files:
file = request.files[image_field_name]
if file and file.filename and allowed_file(file.filename): if file and file.filename and allowed_file(file.filename):
extension = file.filename.rsplit('.', 1)[1].lower()
image_path = f"/images/{product_id}.{extension}"
# Read image data # Read image data
image_data = file.read() file.seek(0)
img_data = file.read()
# Determine mime type # Determine mime type
extension = file.filename.rsplit('.', 1)[1].lower()
mime_types = { mime_types = {
'jpg': 'image/jpeg', 'jpg': 'image/jpeg',
'jpeg': 'image/jpeg', 'jpeg': 'image/jpeg',
'png': 'image/png', 'png': 'image/png',
'gif': 'image/gif' 'gif': 'image/gif'
} }
image_mime_type = mime_types.get(extension, 'image/jpeg') mime_type = mime_types.get(extension, 'image/jpeg')
# Create product data structure # Store for later saving
product_data = [ images_to_save.append({
{"fieldName": "_id", "label": "Item ID", "value": product_id, "editable": "false", "fieldType": "TEXT"}, 'field_name': field_name,
{"fieldName": "_name", "label": "Product Name", "value": name, "editable": "true", "fieldType": "TEXT"}, 'data': img_data,
{"fieldName": "_price", "label": "Sale Price", "value": f"${price}", "editable": "true", "fieldType": "TEXT"}, 'mime_type': mime_type
{"fieldName": "_image", "label": "Image", "value": image_path, "editable": "false", "fieldType": "IMAGE_URI"} })
]
# Set the image URL path
image_path = f"/images/{product_id}_{field_name}.{extension}"
# For backward compatibility with _image field
if field_name == '_image':
image_data = img_data
image_mime_type = mime_type
else:
image_path = ""
else:
image_path = ""
product_data.append({
"fieldName": field_name,
"label": field['label'],
"value": image_path,
"editable": field['editable'],
"fieldType": field['fieldType']
})
else:
# Get value from form
value = request.form.get(f'field_{field_name}', '')
product_data.append({
"fieldName": field_name,
"label": field['label'],
"value": value,
"editable": field['editable'],
"fieldType": field['fieldType']
})
# Also save fields that aren't in custom fields (for backward compatibility)
if '_name' not in [f['fieldName'] for f in custom_fields]:
name = request.form.get('name', '')
if name:
product_data.append({
"fieldName": "_name",
"label": "Product Name",
"value": name,
"editable": "true",
"fieldType": "TEXT"
})
if '_price' not in [f['fieldName'] for f in custom_fields]:
price = request.form.get('price', '')
if price:
product_data.append({
"fieldName": "_price",
"label": "Sale Price",
"value": f"${price}",
"editable": "true",
"fieldType": "TEXT"
})
# Save to database # Save to database
database.save_product(product_id, tenant_id, product_data, image_data, image_mime_type) database.save_product(product_id, tenant_id, product_data, image_data, image_mime_type)
# Save additional images
for img in images_to_save:
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(url_for('admin.index', tenant_id=tenant_id))
return render_template('admin/add_product.html', tenant_id=tenant_id) return render_template('admin/add_product.html', tenant_id=tenant_id, custom_fields=custom_fields)
def edit_product(tenant_id, product_id): def edit_product(tenant_id, product_id):
products = load_products(tenant_id) products = load_products(tenant_id)
custom_fields = database.get_custom_ar_fields(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(url_for('admin.index', tenant_id=tenant_id))
if request.method == 'POST': if request.method == 'POST':
# Get form data # Handle backward compatibility image upload
name = request.form.get('name')
price = request.form.get('price')
# Basic validation
if not name or not price:
flash('Name and Price are required fields.')
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id)
# Update name and price
for field in products[product_id]:
if field["fieldName"] == "_name":
field["value"] = name
elif field["fieldName"] == "_price":
field["value"] = f"${price}"
# Handle image upload
image_data = None image_data = None
image_mime_type = None image_mime_type = None
if 'image' in request.files and request.files['image'].filename: # Process images after product is saved
file = request.files['image'] images_to_save = []
# Update fields based on form data
updated_product = []
# Always include _id field
for field in products[product_id]:
if field["fieldName"] == "_id":
updated_product.append(field)
break
# Process each custom field
for custom_field in custom_fields:
field_name = custom_field['fieldName']
if field_name == '_id':
continue
# Find existing field data
existing_field = None
for field in products[product_id]:
if field["fieldName"] == field_name:
existing_field = field.copy()
break
if not existing_field:
existing_field = {
"fieldName": field_name,
"label": custom_field['label'],
"value": "",
"editable": custom_field['editable'],
"fieldType": custom_field['fieldType']
}
if custom_field['fieldType'] == 'IMAGE_URI':
# Check if new image was uploaded
image_field_name = f'image_{field_name}'
if image_field_name in request.files and request.files[image_field_name].filename:
file = request.files[image_field_name]
if allowed_file(file.filename): if allowed_file(file.filename):
# Read new image data # Read image data
image_data = file.read() file.seek(0)
img_data = file.read()
# Determine mime type # Determine mime type
extension = file.filename.rsplit('.', 1)[1].lower() extension = file.filename.rsplit('.', 1)[1].lower()
@@ -139,21 +240,65 @@ def edit_product(tenant_id, product_id):
'png': 'image/png', 'png': 'image/png',
'gif': 'image/gif' 'gif': 'image/gif'
} }
image_mime_type = mime_types.get(extension, 'image/jpeg') mime_type = mime_types.get(extension, 'image/jpeg')
# Update image path in product data # Store for later saving
image_path = f"/images/{product_id}.{extension}" images_to_save.append({
'field_name': field_name,
'data': img_data,
'mime_type': mime_type
})
# Update image path
existing_field["value"] = f"/images/{product_id}_{field_name}.{extension}"
# For backward compatibility with _image field
if field_name == '_image':
image_data = img_data
image_mime_type = mime_type
else:
# Get value from form
value = request.form.get(f'field_{field_name}', '')
if field_name == '_price' and value and not value.startswith('$'):
value = f"${value}"
existing_field["value"] = value
updated_product.append(existing_field)
# Handle backward compatibility fields
if '_name' not in [f['fieldName'] for f in custom_fields]:
name = request.form.get('name', '')
if name:
for field in products[product_id]: for field in products[product_id]:
if field["fieldName"] == "_image": if field["fieldName"] == "_name":
field["value"] = image_path field["value"] = name
updated_product.append(field)
break
if '_price' not in [f['fieldName'] for f in custom_fields]:
price = request.form.get('price', '')
if price:
for field in products[product_id]:
if field["fieldName"] == "_price":
field["value"] = f"${price}"
updated_product.append(field)
break
# Save to database # Save to database
database.save_product(product_id, tenant_id, products[product_id], image_data, image_mime_type) database.save_product(product_id, tenant_id, updated_product, image_data, image_mime_type)
# Save additional images
for img in images_to_save:
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(url_for('admin.index', tenant_id=tenant_id))
return render_template('admin/edit_product.html', product_id=product_id, product=products[product_id], tenant_id=tenant_id) return render_template('admin/edit_product.html',
product_id=product_id,
product=products[product_id],
tenant_id=tenant_id,
custom_fields=custom_fields)
def delete_product(tenant_id, product_id): def delete_product(tenant_id, product_id):
products = load_products(tenant_id) products = load_products(tenant_id)
@@ -245,3 +390,64 @@ def manage_credentials(tenant_id):
flash('Username and password are required.') flash('Username and password are required.')
return render_template('admin/credentials.html', tenant=tenant, tenant_id=tenant_id) return render_template('admin/credentials.html', tenant=tenant, tenant_id=tenant_id)
def manage_ar_fields(tenant_id):
"""Manage custom AR content fields"""
tenant = database.get_or_create_tenant(tenant_id)
if request.method == 'POST':
action = request.form.get('action')
if action == 'add':
field_data = {
'fieldName': request.form.get('fieldName'),
'label': request.form.get('label'),
'fieldType': request.form.get('fieldType'),
'editable': request.form.get('editable', 'true'),
'displayOrder': int(request.form.get('displayOrder', 0))
}
if field_data['fieldName'] and field_data['label']:
database.save_custom_ar_field(tenant_id, field_data)
flash('Custom field added successfully!')
else:
flash('Field name and label are required.')
elif action == 'delete':
field_id = request.form.get('field_id')
if field_id:
database.delete_custom_ar_field(tenant_id, int(field_id))
flash('Custom field deleted successfully!')
elif action == 'update':
field_data = {
'id': int(request.form.get('field_id')),
'fieldName': request.form.get('fieldName'),
'label': request.form.get('label'),
'fieldType': request.form.get('fieldType'),
'editable': request.form.get('editable', 'true'),
'displayOrder': int(request.form.get('displayOrder', 0))
}
if field_data['fieldName'] and field_data['label']:
database.save_custom_ar_field(tenant_id, field_data)
flash('Custom field updated successfully!')
else:
flash('Field name and label are required.')
return redirect(url_for('admin.manage_ar_fields', tenant_id=tenant_id))
# Get existing custom fields
custom_fields = database.get_custom_ar_fields(tenant_id)
# Available field types
field_types = [
'TEXT',
'IMAGE_URI'
]
return render_template('admin/ar_fields.html',
tenant=tenant,
tenant_id=tenant_id,
custom_fields=custom_fields,
field_types=field_types)

View File

@@ -22,29 +22,49 @@
<div class="form-text">Must be unique. This will be used as the barcode for AR content.</div> <div class="form-text">Must be unique. This will be used as the barcode for AR content.</div>
</div> </div>
{% for field in custom_fields %}
{% if field.fieldName != '_id' %}
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Product Name *</label> <label for="field_{{ field.fieldName }}" class="form-label">{{ field.label }}</label>
<input type="text" class="form-control" id="name" name="name" required> {% if field.fieldType == 'IMAGE_URI' %}
<div class="form-text">Enter a descriptive name for the product.</div> <input type="file" class="form-control" id="image_{{ field.fieldName }}" name="image_{{ field.fieldName }}" accept="image/*" onchange="previewImage(this, 'preview_{{ field.fieldName }}')">
</div>
<div class="mb-3">
<label for="price" class="form-label">Price *</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="price" name="price" required pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
</div>
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">Product Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
<div class="form-text">Supported formats: .png, .jpg, .jpeg, .gif</div> <div class="form-text">Supported formats: .png, .jpg, .jpeg, .gif</div>
<div class="mt-2"> <div class="mt-2">
<img id="image-preview" class="image-preview" style="display: none;"> <img id="preview_{{ field.fieldName }}" class="image-preview" style="display: none; max-width: 200px;">
</div>
{% elif field.fieldType == 'TEXT' %}
{% if field.fieldName == '_price' %}
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
</div>
{% else %}
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}">
{% endif %}
{% else %}
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}">
{% endif %}
</div>
{% endif %}
{% endfor %}
<!-- Fallback fields for backward compatibility -->
{% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %}
<div class="mb-3">
<label for="name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="name" name="name">
</div>
{% endif %}
{% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %}
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
<input type="text" class="form-control" id="price" name="price" pattern="[0-9]+(\.[0-9]{1,2})?" placeholder="49.99">
</div> </div>
</div> </div>
{% 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="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a>
@@ -59,8 +79,8 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
function previewImage(input) { function previewImage(input, previewId) {
var preview = document.getElementById('image-preview'); var preview = document.getElementById(previewId || 'image-preview');
if (input.files && input.files[0]) { if (input.files && input.files[0]) {
var reader = new FileReader(); var reader = new FileReader();

View File

@@ -0,0 +1,170 @@
{% extends "layout.html" %}
{% block title %}Manage AR Fields - KCAP Admin{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>Manage AR Content Fields</h1>
<p>Configure the fields that will be returned by the /arcontentfields and /arinfo endpoints.</p>
</div>
<a href="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Back to Admin</a>
</div>
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Current AR Fields</h5>
</div>
<div class="card-body">
{% if custom_fields %}
<table class="table table-bordered">
<thead>
<tr>
<th>Field Name</th>
<th>Label</th>
<th>Type</th>
<th>Editable</th>
<th>Display Order</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for field in custom_fields %}
<tr>
<td>{{ field.fieldName }}</td>
<td>{{ field.label }}</td>
<td>{{ field.fieldType }}</td>
<td>{{ field.editable }}</td>
<td>{{ field.displayOrder }}</td>
<td>
<button class="btn btn-sm btn-primary"
data-id="{{ field.id }}"
data-fieldname="{{ field.fieldName }}"
data-label="{{ field.label }}"
data-fieldtype="{{ field.fieldType }}"
data-editable="{{ field.editable }}"
data-displayorder="{{ field.displayOrder }}"
onclick="editField(this)">Edit</button>
<form method="POST" style="display: inline-block;">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="field_id" value="{{ field.id }}">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Are you sure you want to delete this field?')">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No custom fields defined. Default fields will be used.</p>
{% endif %}
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Add/Edit AR Field</h5>
</div>
<div class="card-body">
<form method="POST" id="fieldForm">
<input type="hidden" name="action" value="add" id="formAction">
<input type="hidden" name="field_id" value="" id="fieldId">
<div class="form-group mb-3">
<label for="fieldName">Field Name</label>
<input type="text" class="form-control" id="fieldName" name="fieldName" required>
<small class="form-text text-muted">Use underscore prefix for system fields (e.g., _id, _price)</small>
<small class="form-text text-warning" id="fieldNameWarning" style="display: none;">Field name cannot be changed when editing</small>
</div>
<div class="form-group mb-3">
<label for="label">Label</label>
<input type="text" class="form-control" id="label" name="label" required>
</div>
<div class="form-group mb-3">
<label for="fieldType">Field Type</label>
<select class="form-control" id="fieldType" name="fieldType" required>
{% for type in field_types %}
<option value="{{ type }}">{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="form-group mb-3">
<label for="editable">Editable</label>
<select class="form-control" id="editable" name="editable">
<option value="true">Yes</option>
<option value="false">No</option>
</select>
</div>
<div class="form-group mb-3">
<label for="displayOrder">Display Order</label>
<input type="number" class="form-control" id="displayOrder" name="displayOrder" value="0">
</div>
<button type="submit" class="btn btn-primary">Save Field</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">Cancel</button>
</form>
</div>
</div>
<script>
function editField(button) {
// Get data from button attributes
var fieldData = {
id: button.getAttribute('data-id'),
fieldName: button.getAttribute('data-fieldname'),
label: button.getAttribute('data-label'),
fieldType: button.getAttribute('data-fieldtype'),
editable: button.getAttribute('data-editable'),
displayOrder: button.getAttribute('data-displayorder')
};
// Populate form
document.getElementById('formAction').value = 'update';
document.getElementById('fieldId').value = fieldData.id;
document.getElementById('fieldName').value = fieldData.fieldName;
document.getElementById('label').value = fieldData.label;
document.getElementById('fieldType').value = fieldData.fieldType;
document.getElementById('editable').value = fieldData.editable;
document.getElementById('displayOrder').value = fieldData.displayOrder;
// Disable field name when editing and show warning
document.getElementById('fieldName').disabled = true;
document.getElementById('fieldNameWarning').style.display = 'block';
// Update form heading
var formCard = document.querySelector('.card:last-child .card-header h5');
if (formCard) {
formCard.textContent = 'Edit AR Field';
}
// Change button text
document.querySelector('button[type="submit"]').textContent = 'Update Field';
// Scroll to form
document.getElementById('fieldForm').scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formAction').value = 'add';
document.getElementById('fieldId').value = '';
document.getElementById('fieldForm').reset();
// Enable field name and hide warning
document.getElementById('fieldName').disabled = false;
document.getElementById('fieldNameWarning').style.display = 'none';
// Reset form heading
var formCard = document.querySelector('.card:last-child .card-header h5');
if (formCard) {
formCard.textContent = 'Add/Edit AR Field';
}
// Reset button text
document.querySelector('button[type="submit"]').textContent = 'Save Field';
}
</script>
{% endblock %}

View File

@@ -17,52 +17,80 @@
<div class="form-text">Product ID cannot be changed.</div> <div class="form-text">Product ID cannot be changed.</div>
</div> </div>
{% for field in custom_fields %}
{% if field.fieldName != '_id' %}
<div class="mb-3"> <div class="mb-3">
<label for="name" class="form-label">Product Name *</label> <label for="field_{{ field.fieldName }}" class="form-label">{{ field.label }}</label>
{% for field in product %}
{% if field.fieldName == '_name' %} {% set current_value = '' %}
<input type="text" class="form-control" id="name" name="name" required value="{{ field.value }}"> {% for prod_field in product %}
{% if prod_field.fieldName == field.fieldName %}
{% set current_value = prod_field.value %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="form-text">Enter a descriptive name for the product.</div>
</div>
<div class="mb-3"> {% if field.fieldType == 'IMAGE_URI' %}
<label for="price" class="form-label">Price *</label> <input type="file" class="form-control" id="image_{{ field.fieldName }}" name="image_{{ field.fieldName }}" accept="image/*" onchange="previewImage(this, 'preview_new_{{ field.fieldName }}')">
<div class="input-group">
<span class="input-group-text">$</span>
{% for field in product %}
{% if field.fieldName == '_price' %}
{% set price_value = field.value|replace('$', '') %}
<input type="text" class="form-control" id="price" name="price" required
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}">
{% endif %}
{% endfor %}
</div>
<div class="form-text">Enter the price in decimal format (e.g., 49.99)</div>
</div>
<div class="mb-3">
<label for="image" class="form-label">Product Image</label>
<input type="file" class="form-control" id="image" name="image" accept="image/*" onchange="previewImage(this)">
<div class="form-text">Leave empty to keep the current image.</div> <div class="form-text">Leave empty to keep the current image.</div>
{% if current_value %}
<div class="mt-3"> <div class="mt-3">
<label class="form-label">Current Image:</label> <label class="form-label">Current Image:</label>
{% for field in product %}
{% if field.fieldName == '_image' %}
<div> <div>
<img src="{{ field.value }}" alt="Current product image" class="image-preview"> <img src="{{ current_value }}" alt="Current image" class="image-preview" style="max-width: 200px;">
</div>
</div> </div>
{% endif %} {% endif %}
{% endfor %}
</div>
<div class="mt-2"> <div class="mt-2">
<label class="form-label">New Image Preview:</label> <label class="form-label">New Image Preview:</label>
<img id="image-preview" class="image-preview" style="display: none;"> <img id="preview_new_{{ field.fieldName }}" class="image-preview" style="display: none; max-width: 200px;">
</div>
{% elif field.fieldType == 'TEXT' %}
{% if field.fieldName == '_price' %}
<div class="input-group">
<span class="input-group-text">$</span>
{% set price_value = current_value|replace('$', '') %}
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}"
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}" placeholder="49.99">
</div>
{% else %}
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}" value="{{ current_value }}">
{% endif %}
{% else %}
<input type="text" class="form-control" id="field_{{ field.fieldName }}" name="field_{{ field.fieldName }}" value="{{ current_value }}">
{% endif %}
</div>
{% endif %}
{% endfor %}
<!-- Fallback fields for backward compatibility -->
{% if not custom_fields|selectattr('fieldName', 'equalto', '_name')|list %}
{% for field in product %}
{% if field.fieldName == '_name' %}
<div class="mb-3">
<label for="name" class="form-label">Product Name</label>
<input type="text" class="form-control" id="name" name="name" value="{{ field.value }}">
</div>
{% endif %}
{% endfor %}
{% endif %}
{% if not custom_fields|selectattr('fieldName', 'equalto', '_price')|list %}
{% for field in product %}
{% if field.fieldName == '_price' %}
<div class="mb-3">
<label for="price" class="form-label">Price</label>
<div class="input-group">
<span class="input-group-text">$</span>
{% set price_value = field.value|replace('$', '') %}
<input type="text" class="form-control" id="price" name="price"
pattern="[0-9]+(\.[0-9]{1,2})?" value="{{ price_value }}" placeholder="49.99">
</div> </div>
</div> </div>
{% endif %}
{% endfor %}
{% 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="{{ url_for('admin.index', tenant_id=tenant_id) }}" class="btn btn-secondary">Cancel</a>
@@ -77,8 +105,8 @@
{% block extra_js %} {% block extra_js %}
<script> <script>
function previewImage(input) { function previewImage(input, previewId) {
var preview = document.getElementById('image-preview'); var preview = document.getElementById(previewId || 'image-preview');
if (input.files && input.files[0]) { if (input.files && input.files[0]) {
var reader = new FileReader(); var reader = new FileReader();

View File

@@ -9,6 +9,7 @@
<strong>Current Tenant:</strong> {{ tenant.name }} (ID: {{ tenant.id }}) <strong>Current Tenant:</strong> {{ tenant.name }} (ID: {{ tenant.id }})
<div class="mt-2"> <div class="mt-2">
<a href="{{ url_for('admin.manage_credentials', tenant_id=tenant.id) }}" class="btn btn-sm btn-secondary">Manage Credentials</a> <a href="{{ url_for('admin.manage_credentials', tenant_id=tenant.id) }}" class="btn btn-sm btn-secondary">Manage Credentials</a>
<a href="{{ url_for('admin.manage_ar_fields', tenant_id=tenant.id) }}" class="btn btn-sm btn-secondary">Manage AR Fields</a>
<a href="/" class="btn btn-sm btn-outline-secondary">Switch Tenant</a> <a href="/" class="btn btn-sm btn-outline-secondary">Switch Tenant</a>
</div> </div>
</div> </div>

View File

@@ -7,38 +7,47 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="jumbotron"> <div class="jumbotron">
<h1 class="display-4">KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}</h1> <h1 class="display-4">KCAP Demo Server{% if tenant %} - {{ tenant.name }}{% endif %}</h1>
<p class="lead">A simple Flask API for demonstrating AR content retrieval for barcode scanning applications.</p> <p class="lead">Manage your product catalog and AR content for barcode scanning applications.</p>
<hr class="my-4"> <hr class="my-4">
<p>This server simulates the Knox Capture API for AR overlays and includes endpoints for managing product attributes.</p>
<div class="mt-4">
<h2>API Endpoints</h2>
<ul class="list-group">
<li class="list-group-item">
<strong>Login:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/login</code>
<p>Authenticates using Basic Auth with tenant-specific credentials.</p>
</li>
<li class="list-group-item">
<strong>Content Fields:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/arcontentfields</code>
<p>Returns a list of available attributes (e.g., item ID, price, image URI).</p>
</li>
<li class="list-group-item">
<strong>AR Info:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/arinfo?barcode=123456</code>
<p>Returns product details for a given barcode, including image URLs.</p>
</li>
<li class="list-group-item">
<strong>Static Image Server:</strong> <code>GET {% if tenant %}/{{ tenant.id }}{% endif %}/images/123456.png</code>
<p>Serves product images stored in the database.</p>
</li>
</ul>
</div>
<div class="mt-4"> <div class="mt-4">
<h2>Quick Actions</h2>
<div class="row">
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">Product Management</h5>
<p class="card-text">Add, edit, and manage your product catalog with custom fields.</p>
{% if tenant %} {% if tenant %}
<a href="/{{ tenant.id }}/admin/" class="btn btn-primary btn-lg">Go to Admin Interface</a> <a href="/{{ tenant.id }}/admin/" class="btn btn-primary">Manage Products</a>
{% else %} {% endif %}
<a href="/admin" class="btn btn-primary btn-lg">Go to Admin Interface</a> </div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">AR Field Configuration</h5>
<p class="card-text">Customize the fields returned by AR content endpoints.</p>
{% if tenant %}
<a href="/{{ tenant.id }}/admin/ar_fields" class="btn btn-primary">Configure AR Fields</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="mt-4">
<h2>Getting Started</h2>
<ol>
<li>Configure your AR fields to define what information is returned for products</li>
<li>Add products to your catalog with the configured fields</li>
<li>Generate barcodes for your products</li>
<li>Use the AR endpoints in your scanning application</li>
</ol>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}