Add QR code configuration section to admin dashboard

- Add new QR code settings section in admin/configurations page
- QR codes disabled by default in database on creation
- Include size, error correction level, and display options
- Add API endpoints for QR code configuration management
- Update barcode settings to be enabled by default
- Add JavaScript handlers for QR code settings with validation
- Create database migration for QR code configuration settings
parent 4d37119a
......@@ -1542,7 +1542,7 @@ class Migration_021_AddBarcodeConfiguration(DatabaseMigration):
barcode_configs = [
{
'key': 'barcode.enabled',
'value': 'false',
'value': 'true',
'value_type': 'bool',
'description': 'Enable barcode generation for betting tickets',
'is_system': False
......@@ -1628,6 +1628,100 @@ class Migration_021_AddBarcodeConfiguration(DatabaseMigration):
logger.error(f"Failed to remove barcode configuration: {e}")
return False
class Migration_022_AddQRCodeConfiguration(DatabaseMigration):
"""Add QR code configuration settings for betting system"""
def __init__(self):
super().__init__("022", "Add QR code configuration settings for betting system")
def up(self, db_manager) -> bool:
"""Add QR code configuration to configuration table"""
try:
with db_manager.engine.connect() as conn:
# Add QR code configuration settings
qrcode_configs = [
{
'key': 'qrcode.enabled',
'value': 'false',
'value_type': 'bool',
'description': 'Enable QR code generation for betting tickets',
'is_system': False
},
{
'key': 'qrcode.size',
'value': '200',
'value_type': 'int',
'description': 'QR code image size in pixels (square)',
'is_system': False
},
{
'key': 'qrcode.error_correction',
'value': 'M',
'value_type': 'string',
'description': 'QR code error correction level: L, M, Q, H',
'is_system': False
},
{
'key': 'qrcode.show_on_thermal',
'value': 'true',
'value_type': 'bool',
'description': 'Show QR code on thermal printed receipts',
'is_system': False
},
{
'key': 'qrcode.show_on_verification',
'value': 'true',
'value_type': 'bool',
'description': 'Show QR code on bet verification pages',
'is_system': False
}
]
for config in qrcode_configs:
# Check if configuration already exists
result = conn.execute(text("""
SELECT COUNT(*) FROM configuration WHERE key = :key
"""), {'key': config['key']})
exists = result.scalar() > 0
if not exists:
conn.execute(text("""
INSERT INTO configuration
(key, value, value_type, description, is_system, created_at, updated_at)
VALUES (:key, :value, :value_type, :description, :is_system, datetime('now'), datetime('now'))
"""), config)
logger.info(f"Added QR code configuration: {config['key']} = {config['value']}")
else:
logger.info(f"QR code configuration already exists: {config['key']}")
conn.commit()
logger.info("QR code configuration settings added successfully")
return True
except Exception as e:
logger.error(f"Failed to add QR code configuration: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove QR code configuration settings"""
try:
with db_manager.engine.connect() as conn:
# Remove all QR code-related configurations
conn.execute(text("""
DELETE FROM configuration
WHERE key LIKE 'qrcode.%'
"""))
conn.commit()
logger.info("QR code configuration settings removed")
return True
except Exception as e:
logger.error(f"Failed to remove QR code configuration: {e}")
return False
# Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(),
......@@ -1651,6 +1745,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_019_AddPaidFieldToBets(),
Migration_020_FixBetDetailsForeignKey(),
Migration_021_AddBarcodeConfiguration(),
Migration_022_AddQRCodeConfiguration(),
]
......
......@@ -265,6 +265,13 @@ def configuration():
barcode_show_on_thermal = main_bp.db_manager.get_config_value('barcode.show_on_thermal', True) if main_bp.db_manager else True
barcode_show_on_verification = main_bp.db_manager.get_config_value('barcode.show_on_verification', True) if main_bp.db_manager else True
# Load QR code configuration from database
qrcode_enabled = main_bp.db_manager.get_config_value('qrcode.enabled', False) if main_bp.db_manager else False
qrcode_size = main_bp.db_manager.get_config_value('qrcode.size', 200) if main_bp.db_manager else 200
qrcode_error_correction = main_bp.db_manager.get_config_value('qrcode.error_correction', 'M') if main_bp.db_manager else 'M'
qrcode_show_on_thermal = main_bp.db_manager.get_config_value('qrcode.show_on_thermal', True) if main_bp.db_manager else True
qrcode_show_on_verification = main_bp.db_manager.get_config_value('qrcode.show_on_verification', True) if main_bp.db_manager else True
config_data.update({
# General settings
'app_name': general_config.get('app_name', 'MbetterClient'),
......@@ -301,7 +308,14 @@ def configuration():
'barcode_width': barcode_width,
'barcode_height': barcode_height,
'barcode_show_on_thermal': barcode_show_on_thermal,
'barcode_show_on_verification': barcode_show_on_verification
'barcode_show_on_verification': barcode_show_on_verification,
# QR code settings
'qrcode_enabled': qrcode_enabled,
'qrcode_size': qrcode_size,
'qrcode_error_correction': qrcode_error_correction,
'qrcode_show_on_thermal': qrcode_show_on_thermal,
'qrcode_show_on_verification': qrcode_show_on_verification
})
except Exception as e:
logger.warning(f"Error loading configuration values: {e}")
......@@ -4379,6 +4393,89 @@ def set_barcode_settings():
return jsonify({"error": str(e)}), 500
# QR Code Settings API routes
@api_bp.route('/qrcode-settings')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_qrcode_settings():
"""Get QR code configuration"""
try:
if api_bp.db_manager:
settings = {
'enabled': api_bp.db_manager.get_config_value('qrcode.enabled', False),
'size': api_bp.db_manager.get_config_value('qrcode.size', 200),
'error_correction': api_bp.db_manager.get_config_value('qrcode.error_correction', 'M'),
'show_on_thermal': api_bp.db_manager.get_config_value('qrcode.show_on_thermal', True),
'show_on_verification': api_bp.db_manager.get_config_value('qrcode.show_on_verification', True)
}
else:
# Default settings
settings = {
'enabled': False,
'size': 200,
'error_correction': 'M',
'show_on_thermal': True,
'show_on_verification': True
}
return jsonify({
"success": True,
"settings": settings
})
except Exception as e:
logger.error(f"API get QR code settings error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/qrcode-settings', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def set_qrcode_settings():
"""Set QR code configuration (admin only)"""
try:
data = request.get_json() or {}
enabled = data.get('enabled', False)
size = data.get('size', 200)
error_correction = data.get('error_correction', 'M')
show_on_thermal = data.get('show_on_thermal', True)
show_on_verification = data.get('show_on_verification', True)
# Validation
valid_error_levels = ['L', 'M', 'Q', 'H']
if error_correction not in valid_error_levels:
return jsonify({"error": f"Invalid error correction level. Must be one of: {', '.join(valid_error_levels)}"}), 400
if not isinstance(size, int) or size < 100 or size > 500:
return jsonify({"error": "Size must be an integer between 100 and 500"}), 400
if api_bp.db_manager:
# Save QR code configuration to database
api_bp.db_manager.set_config_value('qrcode.enabled', enabled)
api_bp.db_manager.set_config_value('qrcode.size', size)
api_bp.db_manager.set_config_value('qrcode.error_correction', error_correction)
api_bp.db_manager.set_config_value('qrcode.show_on_thermal', show_on_thermal)
api_bp.db_manager.set_config_value('qrcode.show_on_verification', show_on_verification)
logger.info(f"QR code settings updated - enabled: {enabled}, size: {size}, error_correction: {error_correction}")
return jsonify({
"success": True,
"message": f"QR code settings updated: {'enabled' if enabled else 'disabled'}",
"settings": {
"enabled": enabled,
"size": size,
"error_correction": error_correction,
"show_on_thermal": show_on_thermal,
"show_on_verification": show_on_verification
}
})
else:
return jsonify({"error": "Database manager not available"}), 500
except Exception as e:
logger.error(f"API set QR code settings error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/barcode/<uuid:bet_id>')
def generate_bet_barcode(bet_id):
"""Generate barcode image for bet verification - no authentication required"""
......
......@@ -307,7 +307,7 @@
<form id="barcode-config-form">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="barcode-enabled"
{% if config.barcode_enabled %}checked{% endif %}>
{% if config.barcode_enabled != false %}checked{% endif %}>
<label class="form-check-label" for="barcode-enabled">
Enable Barcode Generation
</label>
......@@ -388,6 +388,80 @@
</div>
</div>
<!-- QR Code Settings -->
<div class="card mb-4">
<div class="card-header">
<h5><i class="fas fa-qrcode"></i> QR Code Settings</h5>
<small class="text-muted">Configure QR code generation for bet verification and thermal printing</small>
</div>
<div class="card-body">
<form id="qrcode-config-form">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="qrcode-enabled"
{% if config.qrcode_enabled %}checked{% endif %}>
<label class="form-check-label" for="qrcode-enabled">
Enable QR Code Generation
</label>
<div class="form-text">Generate QR codes for bet verification and mobile access</div>
</div>
<div class="mb-3" id="qrcode-size-group">
<label for="qrcode-size" class="form-label">QR Code Size (px)</label>
<input type="number" class="form-control" id="qrcode-size"
value="{{ config.qrcode_size or 200 }}" min="100" max="500">
<div class="form-text">Image size in pixels (square, 100-500)</div>
</div>
<div class="mb-3" id="qrcode-error-correction-group">
<label for="qrcode-error-correction" class="form-label">Error Correction Level</label>
<select class="form-select" id="qrcode-error-correction">
<option value="L" {% if config.qrcode_error_correction == 'L' %}selected{% endif %}>L (Low - 7%)</option>
<option value="M" {% if config.qrcode_error_correction == 'M' or not config.qrcode_error_correction %}selected{% endif %}>M (Medium - 15%)</option>
<option value="Q" {% if config.qrcode_error_correction == 'Q' %}selected{% endif %}>Q (Quartile - 25%)</option>
<option value="H" {% if config.qrcode_error_correction == 'H' %}selected{% endif %}>H (High - 30%)</option>
</select>
<div class="form-text">Higher error correction allows damaged QR codes to still be readable</div>
</div>
<div class="row" id="qrcode-options-group">
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="qrcode-show-thermal"
{% if config.qrcode_show_on_thermal %}checked{% endif %}>
<label class="form-check-label" for="qrcode-show-thermal">
Show on Thermal Receipts
</label>
<div class="form-text">Include QR code on printed betting receipts</div>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="qrcode-show-verification"
{% if config.qrcode_show_on_verification %}checked{% endif %}>
<label class="form-check-label" for="qrcode-show-verification">
Show on Verification Page
</label>
<div class="form-text">Display QR code on bet verification page</div>
</div>
</div>
</div>
<div class="mb-3" id="qrcode-preview-group" style="display: none;">
<label class="form-label">QR Code Preview</label>
<div class="border rounded p-3 bg-light text-center">
<div id="qrcode-preview-container">
<small class="text-muted">Preview will appear here when QR code is enabled</small>
</div>
<small class="text-muted d-block mt-2">Sample QR code with bet verification URL</small>
</div>
</div>
<button type="submit" class="btn btn-primary">Save QR Code Settings</button>
</form>
<div id="qrcode-status" class="mt-3"></div>
</div>
</div>
<!-- API Client Debug Section -->
<div class="card mb-5">
<div class="card-header">
......@@ -641,6 +715,8 @@
setupCurrencyHandlers();
loadBarcodeSettings();
setupBarcodeHandlers();
loadQRCodeSettings();
setupQRCodeHandlers();
});
// Load global betting mode configuration
......@@ -997,7 +1073,7 @@
if (data.success) {
const settings = data.settings;
document.getElementById('barcode-enabled').checked = settings.enabled || false;
document.getElementById('barcode-enabled').checked = settings.enabled !== false;
document.getElementById('barcode-standard').value = settings.standard || 'none';
document.getElementById('barcode-width').value = settings.width || 200;
document.getElementById('barcode-height').value = settings.height || 100;
......@@ -1013,7 +1089,7 @@
// Don't show any status on page load - only after user saves settings
} else {
// Use defaults
document.getElementById('barcode-enabled').checked = false;
document.getElementById('barcode-enabled').checked = true;
document.getElementById('barcode-standard').value = 'none';
document.getElementById('barcode-width').value = 200;
document.getElementById('barcode-height').value = 100;
......@@ -1027,7 +1103,7 @@
.catch(error => {
console.error('Error loading barcode settings:', error);
// Use defaults
document.getElementById('barcode-enabled').checked = false;
document.getElementById('barcode-enabled').checked = true;
document.getElementById('barcode-standard').value = 'none';
document.getElementById('barcode-width').value = 200;
document.getElementById('barcode-height').value = 100;
......@@ -1094,5 +1170,186 @@
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Error saving barcode settings: ' + error.message + '</div>';
});
}
// QR Code settings handlers
function setupQRCodeHandlers() {
const enabledCheckbox = document.getElementById('qrcode-enabled');
const sizeInput = document.getElementById('qrcode-size');
const errorCorrectionSelect = document.getElementById('qrcode-error-correction');
const thermalCheckbox = document.getElementById('qrcode-show-thermal');
const verificationCheckbox = document.getElementById('qrcode-show-verification');
const previewGroup = document.getElementById('qrcode-preview-group');
// Handle QR code enabled/disabled state
function updateQRCodeControls() {
const enabled = enabledCheckbox.checked;
// Show/hide preview
previewGroup.style.display = enabled ? 'block' : 'none';
// Enable/disable controls
sizeInput.disabled = !enabled;
errorCorrectionSelect.disabled = !enabled;
thermalCheckbox.disabled = !enabled;
verificationCheckbox.disabled = !enabled;
if (enabled) {
updateQRCodePreview();
}
}
// Update QR code preview
function updateQRCodePreview() {
const container = document.getElementById('qrcode-preview-container');
const size = sizeInput.value;
if (!enabledCheckbox.checked) {
container.innerHTML = '<small class="text-muted">Preview will appear here when QR code is enabled</small>';
return;
}
// Create sample QR code preview using a simple visual representation
const sampleUrl = 'https://example.com/verify/12345678-abcd-ef12-3456-7890abcdef12';
container.innerHTML = `
<div style="border: 1px solid #ddd; padding: 10px; background: white; display: inline-block; text-align: center;">
<div id="qrcode-preview-img" style="width: ${Math.min(size, 150)}px; height: ${Math.min(size, 150)}px; display: flex; align-items: center; justify-content: center; background: #f8f9fa;">
<small style="color: #666;">Generating QR code...</small>
</div>
<small class="d-block mt-1" style="word-break: break-all; font-family: monospace; font-size: 10px;">${sampleUrl.substring(0, 30)}...</small>
</div>
`;
// Generate actual QR code preview
generateQRCodePreview(sampleUrl, size);
}
// Event listeners
enabledCheckbox.addEventListener('change', updateQRCodeControls);
sizeInput.addEventListener('input', updateQRCodePreview);
errorCorrectionSelect.addEventListener('change', updateQRCodePreview);
// Handle form submission
document.getElementById('qrcode-config-form').addEventListener('submit', function(e) {
e.preventDefault();
saveQRCodeSettings();
});
// Initial setup
updateQRCodeControls();
}
// Generate actual QR code preview
function generateQRCodePreview(url, size) {
// Create a simple visual representation of what the QR code would look like
const previewContainer = document.getElementById('qrcode-preview-img');
if (previewContainer) {
// Create QR code-like visual using CSS (simplified representation)
const qrSize = Math.min(size, 150);
const modules = 21; // Typical QR code size
const moduleSize = qrSize / modules;
let qrHtml = '<div style="display: flex; flex-wrap: wrap; width: 100%; height: 100%;">';
for (let i = 0; i < modules * modules; i++) {
// Create a pattern that looks like a QR code
const isBlack = (i % 7 === 0) || (Math.floor(i / modules) % 7 === 0) ||
(i < modules * 7) || (i % modules < 7) ||
(Math.random() > 0.7); // Add some random pattern
qrHtml += `<div style="width: ${moduleSize}px; height: ${moduleSize}px; background: ${isBlack ? '#000' : '#fff'};"></div>`;
}
qrHtml += '</div>';
previewContainer.innerHTML = qrHtml;
}
}
function loadQRCodeSettings() {
fetch('/api/qrcode-settings')
.then(response => response.json())
.then(data => {
if (data.success) {
const settings = data.settings;
document.getElementById('qrcode-enabled').checked = settings.enabled || false;
document.getElementById('qrcode-size').value = settings.size || 200;
document.getElementById('qrcode-error-correction').value = settings.error_correction || 'M';
document.getElementById('qrcode-show-thermal').checked = settings.show_on_thermal !== false;
document.getElementById('qrcode-show-verification').checked = settings.show_on_verification !== false;
// Force update controls after loading settings
setTimeout(() => {
const event = new Event('change');
document.getElementById('qrcode-enabled').dispatchEvent(event);
}, 100);
// Don't show any status on page load - only after user saves settings
} else {
// Use defaults
document.getElementById('qrcode-enabled').checked = false;
document.getElementById('qrcode-size').value = 200;
document.getElementById('qrcode-error-correction').value = 'M';
document.getElementById('qrcode-show-thermal').checked = true;
document.getElementById('qrcode-show-verification').checked = true;
document.getElementById('qrcode-status').innerHTML =
'<div class="alert alert-info"><small><i class="fas fa-info-circle"></i> Using default QR code settings</small></div>';
}
})
.catch(error => {
console.error('Error loading QR code settings:', error);
// Use defaults
document.getElementById('qrcode-enabled').checked = false;
document.getElementById('qrcode-size').value = 200;
document.getElementById('qrcode-error-correction').value = 'M';
document.getElementById('qrcode-show-thermal').checked = true;
document.getElementById('qrcode-show-verification').checked = true;
document.getElementById('qrcode-status').innerHTML =
'<div class="alert alert-warning"><small><i class="fas fa-exclamation-triangle"></i> Error loading QR code settings, using defaults</small></div>';
});
}
function saveQRCodeSettings() {
const statusDiv = document.getElementById('qrcode-status');
const settings = {
enabled: document.getElementById('qrcode-enabled').checked,
size: parseInt(document.getElementById('qrcode-size').value),
error_correction: document.getElementById('qrcode-error-correction').value,
show_on_thermal: document.getElementById('qrcode-show-thermal').checked,
show_on_verification: document.getElementById('qrcode-show-verification').checked
};
// Validation
if (settings.size < 100 || settings.size > 500) {
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> QR code size must be between 100 and 500 pixels</div>';
return;
}
statusDiv.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Saving QR code settings...</div>';
fetch('/api/qrcode-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings)
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + data.message + '</div>';
// Auto-hide success message after 3 seconds
setTimeout(() => {
statusDiv.innerHTML = '<div class="alert alert-success"><small><i class="fas fa-check-circle"></i> QR code settings saved</small></div>';
}, 3000);
} else {
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Failed to save QR code settings: ' + data.error + '</div>';
}
})
.catch(error => {
console.error('Error saving QR code settings:', error);
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Error saving QR code settings: ' + error.message + '</div>';
});
}
</script>
{% endblock %}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment