Implement keyboard navigation and improve receipt printing

- Add keyboard navigation to /bets and /bets/new pages (admin and cashier)
- Add keyboard navigation to /cashier/verify-bet page with auto-focus
- Implement conditional QR code and barcode printing based on settings
- Remove fist icon and MBETTER title from printed receipts
- Remove FIXTURE, ITEMS, and STATUS lines from receipts
- Add python-barcode dependency for barcode generation
- Update PyInstaller build script with barcode imports
- Apply receipt changes to all bet list and detail pages
parent 35f6eccf
...@@ -197,6 +197,15 @@ def collect_hidden_imports() -> List[str]: ...@@ -197,6 +197,15 @@ def collect_hidden_imports() -> List[str]:
# Other dependencies # Other dependencies
'packaging', 'packaging',
'pkg_resources', 'pkg_resources',
# Barcode generation
'barcode',
'barcode.codex',
'barcode.ean',
'barcode.isbn',
'barcode.code39',
'barcode.code128',
'barcode.pzn',
] ]
# Conditionally add ffmpeg module if available # Conditionally add ffmpeg module if available
......
...@@ -316,7 +316,7 @@ class GeneralConfig: ...@@ -316,7 +316,7 @@ class GeneralConfig:
app_name: str = "MbetterClient" app_name: str = "MbetterClient"
log_level: str = "INFO" log_level: str = "INFO"
enable_qt: bool = True enable_qt: bool = True
match_interval: int = 20 # Default match interval in minutes match_interval: int = 5 # Default match interval in minutes
@dataclass @dataclass
......
...@@ -387,7 +387,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -387,7 +387,7 @@ class MatchTimerComponent(ThreadedComponent):
try: try:
if self.config_manager: if self.config_manager:
general_config = self.config_manager.get_section_config("general") or {} general_config = self.config_manager.get_section_config("general") or {}
return general_config.get('match_interval', 20) return general_config.get('match_interval', 5)
else: else:
return 20 # Default fallback return 20 # Default fallback
except Exception as e: except Exception as e:
......
...@@ -1046,9 +1046,9 @@ def get_match_interval(): ...@@ -1046,9 +1046,9 @@ def get_match_interval():
try: try:
if api_bp.config_manager: if api_bp.config_manager:
general_config = api_bp.config_manager.get_section_config("general") or {} general_config = api_bp.config_manager.get_section_config("general") or {}
match_interval = general_config.get('match_interval', 20) # Default 20 minutes match_interval = general_config.get('match_interval', 5) # Default 5 minutes
else: else:
match_interval = 20 # Default fallback match_interval = 5 # Default fallback
return jsonify({ return jsonify({
"success": True, "success": True,
...@@ -1778,9 +1778,12 @@ def get_intro_templates(): ...@@ -1778,9 +1778,12 @@ def get_intro_templates():
# Default configuration # Default configuration
default_config = { default_config = {
'templates': [], 'templates': [
'default_show_time': '00:30', {'name': 'fixtures', 'show_time': '00:15'},
'rotating_time': '05:00' {'name': 'match', 'show_time': '00:15'}
],
'default_show_time': '00:15',
'rotating_time': '00:15'
} }
if intro_config: if intro_config:
...@@ -2390,9 +2393,9 @@ def get_match_timer_config(): ...@@ -2390,9 +2393,9 @@ def get_match_timer_config():
try: try:
if api_bp.config_manager: if api_bp.config_manager:
general_config = api_bp.config_manager.get_section_config("general") or {} general_config = api_bp.config_manager.get_section_config("general") or {}
match_interval = general_config.get('match_interval', 20) # Default 20 minutes match_interval = general_config.get('match_interval', 5) # Default 5 minutes
else: else:
match_interval = 20 # Default fallback match_interval = 5 # Default fallback
return jsonify({ return jsonify({
"success": True, "success": True,
......
...@@ -429,13 +429,9 @@ function generateReceiptHtml(betData) { ...@@ -429,13 +429,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = ` let receiptHtml = `
<div class="thermal-receipt-content"> <div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon --> <!-- Header -->
<div class="receipt-header"> <div class="receipt-header">
<div class="receipt-logo"> <div class="receipt-title">BETTING SLIP</div>
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
...@@ -514,16 +510,9 @@ function generateReceiptHtml(betData) { ...@@ -514,16 +510,9 @@ function generateReceiptHtml(betData) {
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- QR Code and Barcode --> <!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification"> <div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<div class="receipt-qr"> <!-- QR Code and Barcode will be conditionally added here -->
<div class="qr-code" id="qr-code-${betData.uuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
<div class="receipt-barcode" id="barcode-container-${betData.uuid}" style="display: none;">
<div class="barcode-image" id="barcode-${betData.uuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
...@@ -535,10 +524,9 @@ function generateReceiptHtml(betData) { ...@@ -535,10 +524,9 @@ function generateReceiptHtml(betData) {
</div> </div>
`; `;
// Generate QR code and barcode after inserting HTML // Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => { setTimeout(() => {
generateQRCode(betData.uuid); generateVerificationCodes(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
}, 100); }, 100);
return receiptHtml; return receiptHtml;
...@@ -554,30 +542,88 @@ function generateQRCode(betUuid) { ...@@ -554,30 +542,88 @@ function generateQRCode(betUuid) {
} }
} }
function generateBarcodeForReceipt(betUuid) { function generateVerificationCodes(betUuid) {
// Generate barcode for thermal receipt if enabled const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
if (!verificationContainer) {
return;
}
// Check QR code settings - QR codes should NOT print if disabled
// Default to NOT showing QR codes if API fails
let shouldShowQR = false;
try {
// Try to get QR settings from API
fetch('/api/qrcode-settings')
.then(response => {
if (!response.ok) {
console.warn('QR settings API failed with status:', response.status);
return null;
}
return response.json();
})
.then(qrSettings => {
if (qrSettings && qrSettings.success && qrSettings.settings) {
shouldShowQR = qrSettings.settings.enabled === true && qrSettings.settings.show_on_thermal === true;
console.log('QR settings check result:', shouldShowQR, qrSettings.settings);
} else {
console.warn('Invalid QR settings response:', qrSettings);
shouldShowQR = false; // Default to not showing
}
// Add QR code if enabled
if (shouldShowQR) {
console.log('Adding QR code to receipt');
const qrHtml = `
<div class="receipt-qr">
<div class="qr-code" id="qr-code-${betUuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', qrHtml);
// Generate QR code
const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
}
} else {
console.log('QR code disabled - not adding to receipt');
}
})
.catch(error => {
console.warn('Failed to check QR code settings, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
});
} catch (error) {
console.warn('Error in QR code settings check, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
}
// Check barcode settings
fetch(`/api/barcode-data/${betUuid}`) fetch(`/api/barcode-data/${betUuid}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success && data.enabled && data.barcode_data) { if (data.success && data.enabled && data.barcode_data && data.barcode_data.show_on_thermal && data.barcode_data.image_base64) {
const barcodeData = data.barcode_data; // Add barcode to receipt
const barcodeHtml = `
// Only show barcode if configured for thermal receipts <div class="receipt-barcode" id="barcode-container-${betUuid}">
if (barcodeData.show_on_thermal && barcodeData.image_base64) { <div class="barcode-image" id="barcode-${betUuid}"></div>
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`); <div class="barcode-text">Scan barcode for verification</div>
const barcodeElement = document.getElementById(`barcode-${betUuid}`); </div>
`;
if (barcodeContainer && barcodeElement) { verificationContainer.insertAdjacentHTML('beforeend', barcodeHtml);
// Display the barcode image
barcodeElement.innerHTML = `<img src="data:image/png;base64,${barcodeData.image_base64}" alt="Barcode" class="barcode-img" style="max-width: ${barcodeData.width}px; height: ${barcodeData.height}px;">`; // Display barcode
barcodeContainer.style.display = 'block'; const barcodeElement = document.getElementById(`barcode-${betUuid}`);
} if (barcodeElement) {
barcodeElement.innerHTML = `<img src="data:image/png;base64,${data.barcode_data.image_base64}" alt="Barcode" class="barcode-img" style="width: ${data.barcode_data.width}px; height: ${data.barcode_data.height}px;">`;
} }
} }
}) })
.catch(error => { .catch(error => {
console.warn('Failed to load barcode data:', error); console.warn('Failed to check barcode settings:', error);
// Don't show error to user, just log it - barcodes are optional
}); });
} }
...@@ -594,10 +640,7 @@ function printThermalReceipt() { ...@@ -594,10 +640,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; } body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; } .thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; } .receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-logo { font-size: 24px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; } .receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; } .receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; } .receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line { .receipt-row, .receipt-bet-line, .receipt-total-line {
...@@ -625,16 +668,13 @@ function printThermalReceipt() { ...@@ -625,16 +668,13 @@ function printThermalReceipt() {
color: #000; color: #000;
background: #fff; background: #fff;
} }
.thermal-receipt-content { .thermal-receipt-content {
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 10px;
} }
.receipt-header { text-align: center; margin-bottom: 15px; } .receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-logo { font-size: 28px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; } .receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator { .receipt-separator {
text-align: center; text-align: center;
margin: 12px 0; margin: 12px 0;
......
...@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/verify-bet'; window.location.href = '/verify-bet';
}); });
// Keyboard navigation: Enter key moves to /bets/new
document.addEventListener('keydown', function(event) {
// Only handle Enter key if not in an input field
if (event.key === 'Enter' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA' && event.target.tagName !== 'SELECT') {
event.preventDefault();
window.location.href = '/bets/new';
}
});
// Print receipt button in modal // Print receipt button in modal
const printReceiptBtn = document.getElementById('btn-print-receipt'); const printReceiptBtn = document.getElementById('btn-print-receipt');
if (printReceiptBtn) { if (printReceiptBtn) {
...@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) { ...@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = ` let receiptHtml = `
<div class="thermal-receipt-content"> <div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon --> <!-- Header -->
<div class="receipt-header"> <div class="receipt-header">
<div class="receipt-logo"> <div class="receipt-title">BETTING SLIP</div>
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
...@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) { ...@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) {
<span>DATE:</span> <span>DATE:</span>
<span>${betData.bet_datetime}</span> <span>${betData.bet_datetime}</span>
</div> </div>
<div class="receipt-row">
<span>FIXTURE:</span>
<span>${betData.fixture_id}</span>
</div>
<div class="receipt-row">
<span>ITEMS:</span>
<span>${betData.bet_count}</span>
</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
...@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) { ...@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span> <span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span> <span>${formatCurrency(parseFloat(detail.amount))}</span>
</div> </div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
</div> </div>
`; `;
...@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) { ...@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) {
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- QR Code and Barcode --> <!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification"> <div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<div class="receipt-qr"> <!-- QR Code and Barcode will be conditionally added here -->
<div class="qr-code" id="qr-code-${betData.uuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
<div class="receipt-barcode" id="barcode-container-${betData.uuid}" style="display: none;">
<div class="barcode-image" id="barcode-${betData.uuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
...@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) { ...@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) {
</div> </div>
`; `;
// Generate QR code and barcode after inserting HTML // Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => { setTimeout(() => {
generateQRCode(betData.uuid); generateVerificationCodes(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
}, 100); }, 100);
return receiptHtml; return receiptHtml;
...@@ -673,30 +659,88 @@ function generateQRCode(betUuid) { ...@@ -673,30 +659,88 @@ function generateQRCode(betUuid) {
} }
} }
function generateBarcodeForReceipt(betUuid) { function generateVerificationCodes(betUuid) {
// Generate barcode for thermal receipt if enabled const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
if (!verificationContainer) {
return;
}
// Check QR code settings - QR codes should NOT print if disabled
// Default to NOT showing QR codes if API fails
let shouldShowQR = false;
try {
// Try to get QR settings from API
fetch('/api/qrcode-settings')
.then(response => {
if (!response.ok) {
console.warn('QR settings API failed with status:', response.status);
return null;
}
return response.json();
})
.then(qrSettings => {
if (qrSettings && qrSettings.success && qrSettings.settings) {
shouldShowQR = qrSettings.settings.enabled === true && qrSettings.settings.show_on_thermal === true;
console.log('QR settings check result:', shouldShowQR, qrSettings.settings);
} else {
console.warn('Invalid QR settings response:', qrSettings);
shouldShowQR = false; // Default to not showing
}
// Add QR code if enabled
if (shouldShowQR) {
console.log('Adding QR code to receipt');
const qrHtml = `
<div class="receipt-qr">
<div class="qr-code" id="qr-code-${betUuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', qrHtml);
// Generate QR code
const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
}
} else {
console.log('QR code disabled - not adding to receipt');
}
})
.catch(error => {
console.warn('Failed to check QR code settings, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
});
} catch (error) {
console.warn('Error in QR code settings check, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
}
// Check barcode settings
fetch(`/api/barcode-data/${betUuid}`) fetch(`/api/barcode-data/${betUuid}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success && data.enabled && data.barcode_data) { if (data.success && data.enabled && data.barcode_data && data.barcode_data.show_on_thermal && data.barcode_data.image_base64) {
const barcodeData = data.barcode_data; // Add barcode to receipt
const barcodeHtml = `
// Only show barcode if configured for thermal receipts <div class="receipt-barcode" id="barcode-container-${betUuid}">
if (barcodeData.show_on_thermal && barcodeData.image_base64) { <div class="barcode-image" id="barcode-${betUuid}"></div>
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`); <div class="barcode-text">Scan barcode for verification</div>
const barcodeElement = document.getElementById(`barcode-${betUuid}`); </div>
`;
if (barcodeContainer && barcodeElement) { verificationContainer.insertAdjacentHTML('beforeend', barcodeHtml);
// Display the barcode image
barcodeElement.innerHTML = `<img src="data:image/png;base64,${barcodeData.image_base64}" alt="Barcode" class="barcode-img" style="max-width: ${barcodeData.width}px; height: ${barcodeData.height}px;">`; // Display barcode
barcodeContainer.style.display = 'block'; const barcodeElement = document.getElementById(`barcode-${betUuid}`);
} if (barcodeElement) {
barcodeElement.innerHTML = `<img src="data:image/png;base64,${data.barcode_data.image_base64}" alt="Barcode" class="barcode-img" style="width: ${data.barcode_data.width}px; height: ${data.barcode_data.height}px;">`;
} }
} }
}) })
.catch(error => { .catch(error => {
console.warn('Failed to load barcode data:', error); console.warn('Failed to check barcode settings:', error);
// Don't show error to user, just log it - barcodes are optional
}); });
} }
...@@ -713,10 +757,7 @@ function printThermalReceipt() { ...@@ -713,10 +757,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; } body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; } .thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; } .receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-logo { font-size: 24px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; } .receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; } .receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; } .receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line { .receipt-row, .receipt-bet-line, .receipt-total-line {
...@@ -744,16 +785,13 @@ function printThermalReceipt() { ...@@ -744,16 +785,13 @@ function printThermalReceipt() {
color: #000; color: #000;
background: #fff; background: #fff;
} }
.thermal-receipt-content { .thermal-receipt-content {
max-width: 300px; max-width: 300px;
margin: 0 auto; margin: 0 auto;
padding: 10px; padding: 10px;
} }
.receipt-header { text-align: center; margin-bottom: 15px; } .receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-logo { font-size: 28px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; } .receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator { .receipt-separator {
text-align: center; text-align: center;
margin: 12px 0; margin: 12px 0;
......
...@@ -10,6 +10,42 @@ ...@@ -10,6 +10,42 @@
</div> </div>
</div> </div>
<!-- Keyboard Input Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-keyboard me-2"></i>Match Selection
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-hashtag"></i>
</span>
<input type="text" class="form-control form-control-lg" id="match-input"
placeholder="Enter match number and press Enter" readonly
style="font-size: 1.2rem; font-weight: bold;">
<span class="input-group-text">
<small class="text-muted">Press digits + Enter to select match</small>
</span>
</div>
</div>
<div class="col-md-4 text-end">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Use keyboard navigation for faster betting
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Back Button and Submit Button --> <!-- Back Button and Submit Button -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
...@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() { document.getElementById('btn-submit-bet').addEventListener('click', function() {
submitBet(); submitBet();
}); });
// Initialize keyboard navigation
initializeKeyboardNavigation();
}); });
// Keyboard navigation state
let keyboardState = {
inputBuffer: '',
currentMatch: null,
currentOutcomeIndex: -1,
outcomes: []
};
function initializeKeyboardNavigation() {
const matchInput = document.getElementById('match-input');
// Global keyboard event listener
document.addEventListener('keydown', function(event) {
// Handle digit input for match selection
if (event.key >= '0' && event.key <= '9' && !isInInputField(event.target)) {
event.preventDefault();
keyboardState.inputBuffer += event.key;
matchInput.value = keyboardState.inputBuffer;
matchInput.classList.add('is-valid');
return;
}
// Handle Enter key for match selection
if (event.key === 'Enter' && keyboardState.inputBuffer && !isInInputField(event.target)) {
event.preventDefault();
const matchNumber = parseInt(keyboardState.inputBuffer);
selectMatchByNumber(matchNumber);
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Tab key for outcome navigation (only when in match)
if (event.key === 'Tab' && keyboardState.currentMatch) {
event.preventDefault();
navigateOutcomes();
return;
}
// Handle digits + Enter for bet amount (when outcome is selected)
if (event.key === 'Enter' && keyboardState.currentOutcomeIndex >= 0 && keyboardState.inputBuffer) {
event.preventDefault();
const amount = parseFloat(keyboardState.inputBuffer);
if (amount > 0) {
enterBetAmount(amount);
}
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Ctrl+Enter for submitting bet
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
submitBet();
return;
}
// Handle Escape key for canceling
if (event.key === 'Escape') {
event.preventDefault();
if (keyboardState.currentMatch) {
// Close current match and reset
closeCurrentMatch();
} else {
// Go back to bets page
window.location.href = '/bets';
}
return;
}
});
}
function isInInputField(element) {
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' ||
element.contentEditable === 'true';
}
function selectMatchByNumber(matchNumber) {
// Find match card with this number
const matchCards = document.querySelectorAll('.match-card');
for (const card of matchCards) {
const matchId = card.getAttribute('data-match-id');
const headerText = card.querySelector('h6').textContent;
const matchNumMatch = headerText.match(/Match #(\d+)/);
if (matchNumMatch && parseInt(matchNumMatch[1]) === matchNumber) {
// Open this match
openMatch(card, matchId);
return;
}
}
// Match not found - visual feedback
const matchInput = document.getElementById('match-input');
matchInput.classList.add('is-invalid');
setTimeout(() => matchInput.classList.remove('is-invalid'), 1000);
}
function openMatch(card, matchId) {
// Close any currently open match
closeCurrentMatch();
// Open the selected match
const outcomesDiv = document.getElementById(`outcomes-${matchId}`);
const icon = card.querySelector('.toggle-match i');
if (outcomesDiv && outcomesDiv.style.display === 'none') {
outcomesDiv.style.display = 'block';
icon.className = 'fas fa-chevron-up me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-up me-1"></i>Hide Outcomes';
// Set as current match
keyboardState.currentMatch = matchId;
keyboardState.currentOutcomeIndex = -1;
// Get outcomes for navigation
const outcomeCards = outcomesDiv.querySelectorAll('.card');
keyboardState.outcomes = Array.from(outcomeCards);
// Highlight the match card
card.classList.add('border-primary');
card.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
}
}
function closeCurrentMatch() {
if (keyboardState.currentMatch) {
const card = document.querySelector(`[data-match-id="${keyboardState.currentMatch}"]`);
if (card) {
const outcomesDiv = document.getElementById(`outcomes-${keyboardState.currentMatch}`);
const icon = card.querySelector('.toggle-match i');
outcomesDiv.style.display = 'none';
icon.className = 'fas fa-chevron-down me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-down me-1"></i>Select Outcomes';
// Remove highlighting
card.classList.remove('border-primary');
card.style.boxShadow = '';
}
keyboardState.currentMatch = null;
keyboardState.currentOutcomeIndex = -1;
keyboardState.outcomes = [];
}
}
function navigateOutcomes() {
if (keyboardState.outcomes.length === 0) return;
// Remove previous highlighting
if (keyboardState.currentOutcomeIndex >= 0) {
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.border = '';
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.boxShadow = '';
}
// Move to next outcome
keyboardState.currentOutcomeIndex = (keyboardState.currentOutcomeIndex + 1) % keyboardState.outcomes.length;
// Highlight current outcome
const currentOutcome = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
currentOutcome.style.border = '3px solid #0d6efd';
currentOutcome.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
// Scroll into view
currentOutcome.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function enterBetAmount(amount) {
if (keyboardState.currentOutcomeIndex < 0 || !keyboardState.outcomes[keyboardState.currentOutcomeIndex]) return;
const outcomeCard = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
const amountInput = outcomeCard.querySelector('.amount-input');
if (amountInput) {
amountInput.value = amount.toFixed(2);
amountInput.dispatchEvent(new Event('input')); // Trigger the input event to update summary
// Visual feedback
amountInput.classList.add('is-valid');
setTimeout(() => amountInput.classList.remove('is-valid'), 500);
// Move to next outcome automatically
setTimeout(() => navigateOutcomes(), 200);
}
}
let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] } let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] }
// Function to load and display available matches for betting // Function to load and display available matches for betting
......
...@@ -354,7 +354,7 @@ document.addEventListener('currencySettingsLoaded', function(event) { ...@@ -354,7 +354,7 @@ document.addEventListener('currencySettingsLoaded', function(event) {
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Generate QR code for bet verification // Generate QR code for bet verification
generateBetVerificationQR(); generateBetVerificationQR();
// Cancel entire bet button // Cancel entire bet button
const cancelBetBtn = document.getElementById('btn-cancel-bet'); const cancelBetBtn = document.getElementById('btn-cancel-bet');
if (cancelBetBtn) { if (cancelBetBtn) {
...@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
cancelEntireBet(betUuid); cancelEntireBet(betUuid);
}); });
} }
// Delete bet detail buttons // Delete bet detail buttons
const deleteDetailBtns = document.querySelectorAll('.btn-delete-detail'); const deleteDetailBtns = document.querySelectorAll('.btn-delete-detail');
deleteDetailBtns.forEach(function(btn) { deleteDetailBtns.forEach(function(btn) {
...@@ -372,7 +372,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -372,7 +372,7 @@ document.addEventListener('DOMContentLoaded', function() {
deleteBetDetail(detailId); deleteBetDetail(detailId);
}); });
}); });
// Preview bet button // Preview bet button
const previewBetBtns = document.querySelectorAll('.btn-preview-bet'); const previewBetBtns = document.querySelectorAll('.btn-preview-bet');
previewBetBtns.forEach(function(btn) { previewBetBtns.forEach(function(btn) {
...@@ -390,7 +390,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -390,7 +390,7 @@ document.addEventListener('DOMContentLoaded', function() {
directPrintBet(betId); directPrintBet(betId);
}); });
}); });
// Print receipt button in modal // Print receipt button in modal
const printReceiptBtn = document.getElementById('btn-print-receipt'); const printReceiptBtn = document.getElementById('btn-print-receipt');
if (printReceiptBtn) { if (printReceiptBtn) {
...@@ -398,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -398,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() {
printThermalReceipt(); printThermalReceipt();
}); });
} }
// Mark as paid button // Mark as paid button
const markPaidBtn = document.getElementById('btn-mark-paid'); const markPaidBtn = document.getElementById('btn-mark-paid');
if (markPaidBtn) { if (markPaidBtn) {
...@@ -407,7 +407,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -407,7 +407,7 @@ document.addEventListener('DOMContentLoaded', function() {
markBetAsPaid(betUuid); markBetAsPaid(betUuid);
}); });
} }
// Update current time every second // Update current time every second
setInterval(updateCurrentTime, 1000); setInterval(updateCurrentTime, 1000);
updateCurrentTime(); updateCurrentTime();
...@@ -475,7 +475,7 @@ function generateThermalReceipt(betId) { ...@@ -475,7 +475,7 @@ function generateThermalReceipt(betId) {
const receiptContainer = document.getElementById('thermal-receipt'); const receiptContainer = document.getElementById('thermal-receipt');
const receiptHtml = generateReceiptHtml(window.betData); const receiptHtml = generateReceiptHtml(window.betData);
receiptContainer.innerHTML = receiptHtml; receiptContainer.innerHTML = receiptHtml;
// Show the modal // Show the modal
const modal = new bootstrap.Modal(document.getElementById('printReceiptModal')); const modal = new bootstrap.Modal(document.getElementById('printReceiptModal'));
modal.show(); modal.show();
...@@ -483,21 +483,17 @@ function generateThermalReceipt(betId) { ...@@ -483,21 +483,17 @@ function generateThermalReceipt(betId) {
function generateReceiptHtml(betData) { function generateReceiptHtml(betData) {
const currentDateTime = new Date().toLocaleString(); const currentDateTime = new Date().toLocaleString();
let receiptHtml = ` let receiptHtml = `
<div class="thermal-receipt-content"> <div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon --> <!-- Header -->
<div class="receipt-header"> <div class="receipt-header">
<div class="receipt-logo"> <div class="receipt-title">BETTING SLIP</div>
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- Bet Information --> <!-- Bet Information -->
<div class="receipt-info"> <div class="receipt-info">
<div class="receipt-row"> <div class="receipt-row">
...@@ -517,14 +513,14 @@ function generateReceiptHtml(betData) { ...@@ -517,14 +513,14 @@ function generateReceiptHtml(betData) {
<span>${betData.bet_count}</span> <span>${betData.bet_count}</span>
</div> </div>
</div> </div>
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- Bet Details --> <!-- Bet Details -->
<div class="receipt-bets"> <div class="receipt-bets">
`; `;
let totalAmount = 0; let totalAmount = 0;
betData.bet_details.forEach((detail, index) => { betData.bet_details.forEach((detail, index) => {
totalAmount += parseFloat(detail.amount); totalAmount += parseFloat(detail.amount);
...@@ -548,18 +544,18 @@ function generateReceiptHtml(betData) { ...@@ -548,18 +544,18 @@ function generateReceiptHtml(betData) {
</div> </div>
</div> </div>
`; `;
if (index < betData.bet_details.length - 1) { if (index < betData.bet_details.length - 1) {
receiptHtml += `<div class="receipt-separator">- - - - - - - - - - - - - - - - -</div>`; receiptHtml += `<div class="receipt-separator">- - - - - - - - - - - - - - - - -</div>`;
} }
}); });
receiptHtml += ` receiptHtml += `
</div> </div>
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- Total --> <!-- Total -->
<div class="receipt-total"> <div class="receipt-total">
<div class="receipt-total-line"> <div class="receipt-total-line">
...@@ -567,22 +563,15 @@ function generateReceiptHtml(betData) { ...@@ -567,22 +563,15 @@ function generateReceiptHtml(betData) {
<span>${formatCurrency(totalAmount)}</span> <span>${formatCurrency(totalAmount)}</span>
</div> </div>
</div> </div>
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- QR Code and Barcode --> <!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification"> <div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<div class="receipt-qr"> <!-- QR Code and Barcode will be conditionally added here -->
<div class="qr-code" id="qr-code-${betData.uuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
<div class="receipt-barcode" id="barcode-container-${betData.uuid}" style="display: none;">
<div class="barcode-image" id="barcode-${betData.uuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
<div class="receipt-footer"> <div class="receipt-footer">
<div>Thank you for betting with MBetter!</div> <div>Thank you for betting with MBetter!</div>
...@@ -591,80 +580,97 @@ function generateReceiptHtml(betData) { ...@@ -591,80 +580,97 @@ function generateReceiptHtml(betData) {
</div> </div>
</div> </div>
`; `;
// Generate QR code and barcode after inserting HTML // Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => { setTimeout(() => {
generateQRCode(betData.uuid); generateVerificationCodes(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
}, 100); }, 100);
return receiptHtml; return receiptHtml;
} }
function generateQRCode(betUuid) { function generateVerificationCodes(betUuid) {
// Simple QR code generation using a free service const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
const qrContainer = document.getElementById(`qr-code-${betUuid}`); if (!verificationContainer) {
if (qrContainer) { return;
// Use QR Server API for generating QR code
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
} }
}
function generateBarcodeForReceipt(betUuid) { // Check QR code settings - QR codes should NOT print if disabled
// Generate barcode for thermal receipt if enabled // Default to NOT showing QR codes if API fails
console.log('🔍 BARCODE DEBUG: Starting barcode generation for bet:', betUuid); let shouldShowQR = false;
fetch(`/api/barcode-data/${betUuid}`) try {
.then(response => { // Try to get QR settings from API
console.log('🔍 BARCODE DEBUG: API response status:', response.status); fetch('/api/qrcode-settings')
return response.json(); .then(response => {
}) if (!response.ok) {
.then(data => { console.warn('QR settings API failed with status:', response.status);
console.log('🔍 BARCODE DEBUG: API response data:', data); return null;
}
if (data.success && data.enabled && data.barcode_data) { return response.json();
const barcodeData = data.barcode_data; })
console.log('🔍 BARCODE DEBUG: Barcode data received:', barcodeData); .then(qrSettings => {
if (qrSettings && qrSettings.success && qrSettings.settings) {
// Only show barcode if configured for thermal receipts shouldShowQR = qrSettings.settings.enabled === true && qrSettings.settings.show_on_thermal === true;
if (barcodeData.show_on_thermal && barcodeData.image_base64) { console.log('QR settings check result:', shouldShowQR, qrSettings.settings);
console.log('🔍 BARCODE DEBUG: Thermal receipt barcode enabled, searching for DOM elements...'); } else {
console.warn('Invalid QR settings response:', qrSettings);
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`); shouldShowQR = false; // Default to not showing
const barcodeElement = document.getElementById(`barcode-${betUuid}`); }
console.log('🔍 BARCODE DEBUG: DOM elements found:', { // Add QR code if enabled
container: !!barcodeContainer, if (shouldShowQR) {
element: !!barcodeElement, console.log('Adding QR code to receipt');
containerDisplay: barcodeContainer ? barcodeContainer.style.display : 'not found' const qrHtml = `
}); <div class="receipt-qr">
<div class="qr-code" id="qr-code-${betUuid}"></div>
if (barcodeContainer && barcodeElement) { <div class="qr-text">Scan QR for verification</div>
// Display the barcode image with exact configured dimensions </div>
barcodeElement.innerHTML = `<img src="data:image/png;base64,${barcodeData.image_base64}" alt="Barcode" class="barcode-img" style="width: ${barcodeData.width}px; height: ${barcodeData.height}px;">`; `;
barcodeContainer.style.display = 'block'; verificationContainer.insertAdjacentHTML('beforeend', qrHtml);
console.log('🔍 BARCODE DEBUG: ✅ Barcode displayed successfully!');
} else { // Generate QR code
console.warn('🔍 BARCODE DEBUG: ❌ DOM elements not found for barcode display'); const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
} }
} else { } else {
console.warn('🔍 BARCODE DEBUG: ❌ Barcode not configured for thermal receipts or no image data:', { console.log('QR code disabled - not adding to receipt');
show_on_thermal: barcodeData.show_on_thermal, }
has_image: !!barcodeData.image_base64 })
}); .catch(error => {
console.warn('Failed to check QR code settings, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
});
} catch (error) {
console.warn('Error in QR code settings check, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
}
// Check barcode settings
fetch(`/api/barcode-data/${betUuid}`)
.then(response => response.json())
.then(data => {
if (data.success && data.enabled && data.barcode_data && data.barcode_data.show_on_thermal && data.barcode_data.image_base64) {
// Add barcode to receipt
const barcodeHtml = `
<div class="receipt-barcode" id="barcode-container-${betUuid}">
<div class="barcode-image" id="barcode-${betUuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', barcodeHtml);
// Display barcode
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeElement) {
barcodeElement.innerHTML = `<img src="data:image/png;base64,${data.barcode_data.image_base64}" alt="Barcode" class="barcode-img" style="width: ${data.barcode_data.width}px; height: ${data.barcode_data.height}px;">`;
} }
} else {
console.warn('🔍 BARCODE DEBUG: ❌ API returned invalid data or barcodes disabled:', {
success: data.success,
enabled: data.enabled,
has_barcode_data: !!data.barcode_data
});
} }
}) })
.catch(error => { .catch(error => {
console.error('🔍 BARCODE DEBUG: ❌ API call failed:', error); console.warn('Failed to check barcode settings:', error);
// Don't show error to user, just log it - barcodes are optional
}); });
} }
...@@ -681,10 +687,7 @@ function printThermalReceipt() { ...@@ -681,10 +687,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; } body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; } .thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; } .receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-logo { font-size: 24px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; } .receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; } .receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; } .receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line { .receipt-row, .receipt-bet-line, .receipt-total-line {
...@@ -718,10 +721,7 @@ function printThermalReceipt() { ...@@ -718,10 +721,7 @@ function printThermalReceipt() {
padding: 10px; padding: 10px;
} }
.receipt-header { text-align: center; margin-bottom: 15px; } .receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-logo { font-size: 28px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; } .receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator { .receipt-separator {
text-align: center; text-align: center;
margin: 12px 0; margin: 12px 0;
......
...@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() {
window.location.href = '/cashier/verify-bet'; window.location.href = '/cashier/verify-bet';
}); });
// Keyboard navigation: Enter key moves to /bets/new
document.addEventListener('keydown', function(event) {
// Only handle Enter key if not in an input field
if (event.key === 'Enter' && event.target.tagName !== 'INPUT' && event.target.tagName !== 'TEXTAREA' && event.target.tagName !== 'SELECT') {
event.preventDefault();
window.location.href = '/cashier/bets/new';
}
});
// Status update functions (same as cashier dashboard) // Status update functions (same as cashier dashboard)
function updateVideoStatus() { function updateVideoStatus() {
fetch('/api/video/status') fetch('/api/video/status')
...@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) { ...@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = ` let receiptHtml = `
<div class="thermal-receipt-content"> <div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon --> <!-- Header -->
<div class="receipt-header"> <div class="receipt-header">
<div class="receipt-logo"> <div class="receipt-title">BETTING SLIP</div>
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
...@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) { ...@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) {
<span>DATE:</span> <span>DATE:</span>
<span>${betData.bet_datetime}</span> <span>${betData.bet_datetime}</span>
</div> </div>
<div class="receipt-row">
<span>FIXTURE:</span>
<span>${betData.fixture_id}</span>
</div>
<div class="receipt-row">
<span>ITEMS:</span>
<span>${betData.bet_count}</span>
</div>
</div> </div>
<!-- Separator --> <!-- Separator -->
...@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) { ...@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span> <span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span> <span>${formatCurrency(parseFloat(detail.amount))}</span>
</div> </div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
</div> </div>
`; `;
...@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) { ...@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) {
<!-- Separator --> <!-- Separator -->
<div class="receipt-separator">================================</div> <div class="receipt-separator">================================</div>
<!-- QR Code and Barcode --> <!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification"> <div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<div class="receipt-qr"> <!-- QR Code and Barcode will be conditionally added here -->
<div class="qr-code" id="qr-code-${betData.uuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
<div class="receipt-barcode" id="barcode-container-${betData.uuid}" style="display: none;">
<div class="barcode-image" id="barcode-${betData.uuid}"></div>
<div class="barcode-text">Scan barcode for verification</div>
</div>
</div> </div>
<!-- Footer --> <!-- Footer -->
...@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) { ...@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) {
</div> </div>
`; `;
// Generate QR code and barcode after inserting HTML // Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => { setTimeout(() => {
generateQRCode(betData.uuid); generateVerificationCodes(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
}, 100); }, 100);
return receiptHtml; return receiptHtml;
...@@ -673,30 +659,88 @@ function generateQRCode(betUuid) { ...@@ -673,30 +659,88 @@ function generateQRCode(betUuid) {
} }
} }
function generateBarcodeForReceipt(betUuid) { function generateVerificationCodes(betUuid) {
// Generate barcode for thermal receipt if enabled const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
if (!verificationContainer) {
return;
}
// Check QR code settings - QR codes should NOT print if disabled
// Default to NOT showing QR codes if API fails
let shouldShowQR = false;
try {
// Try to get QR settings from API
fetch('/api/qrcode-settings')
.then(response => {
if (!response.ok) {
console.warn('QR settings API failed with status:', response.status);
return null;
}
return response.json();
})
.then(qrSettings => {
if (qrSettings && qrSettings.success && qrSettings.settings) {
shouldShowQR = qrSettings.settings.enabled === true && qrSettings.settings.show_on_thermal === true;
console.log('QR settings check result:', shouldShowQR, qrSettings.settings);
} else {
console.warn('Invalid QR settings response:', qrSettings);
shouldShowQR = false; // Default to not showing
}
// Add QR code if enabled
if (shouldShowQR) {
console.log('Adding QR code to receipt');
const qrHtml = `
<div class="receipt-qr">
<div class="qr-code" id="qr-code-${betUuid}"></div>
<div class="qr-text">Scan QR for verification</div>
</div>
`;
verificationContainer.insertAdjacentHTML('beforeend', qrHtml);
// Generate QR code
const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
const qrImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=100x100&data=${encodeURIComponent(betUuid)}&format=png`;
qrContainer.innerHTML = `<img src="${qrImageUrl}" alt="QR Code" class="qr-image">`;
}
} else {
console.log('QR code disabled - not adding to receipt');
}
})
.catch(error => {
console.warn('Failed to check QR code settings, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
});
} catch (error) {
console.warn('Error in QR code settings check, defaulting to disabled:', error);
shouldShowQR = false; // Default to not showing on error
}
// Check barcode settings
fetch(`/api/barcode-data/${betUuid}`) fetch(`/api/barcode-data/${betUuid}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success && data.enabled && data.barcode_data) { if (data.success && data.enabled && data.barcode_data && data.barcode_data.show_on_thermal && data.barcode_data.image_base64) {
const barcodeData = data.barcode_data; // Add barcode to receipt
const barcodeHtml = `
// Only show barcode if configured for thermal receipts <div class="receipt-barcode" id="barcode-container-${betUuid}">
if (barcodeData.show_on_thermal && barcodeData.image_base64) { <div class="barcode-image" id="barcode-${betUuid}"></div>
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`); <div class="barcode-text">Scan barcode for verification</div>
const barcodeElement = document.getElementById(`barcode-${betUuid}`); </div>
`;
if (barcodeContainer && barcodeElement) { verificationContainer.insertAdjacentHTML('beforeend', barcodeHtml);
// Display the barcode image
barcodeElement.innerHTML = `<img src="data:image/png;base64,${barcodeData.image_base64}" alt="Barcode" class="barcode-img" style="max-width: ${barcodeData.width}px; height: ${barcodeData.height}px;">`; // Display barcode
barcodeContainer.style.display = 'block'; const barcodeElement = document.getElementById(`barcode-${betUuid}`);
} if (barcodeElement) {
barcodeElement.innerHTML = `<img src="data:image/png;base64,${data.barcode_data.image_base64}" alt="Barcode" class="barcode-img" style="width: ${data.barcode_data.width}px; height: ${data.barcode_data.height}px;">`;
} }
} }
}) })
.catch(error => { .catch(error => {
console.warn('Failed to load barcode data:', error); console.warn('Failed to check barcode settings:', error);
// Don't show error to user, just log it - barcodes are optional
}); });
} }
...@@ -713,10 +757,7 @@ function printThermalReceipt() { ...@@ -713,10 +757,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; } body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; } .thermal-receipt-content { width: 100%; }
.receipt-header { text-align: center; margin-bottom: 10px; } .receipt-header { text-align: center; margin-bottom: 10px; }
.receipt-logo { font-size: 24px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; } .receipt-title { font-size: 18px; font-weight: bold; margin-bottom: 2px; }
.receipt-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; } .receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; } .receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line { .receipt-row, .receipt-bet-line, .receipt-total-line {
...@@ -750,10 +791,7 @@ function printThermalReceipt() { ...@@ -750,10 +791,7 @@ function printThermalReceipt() {
padding: 10px; padding: 10px;
} }
.receipt-header { text-align: center; margin-bottom: 15px; } .receipt-header { text-align: center; margin-bottom: 15px; }
.receipt-logo { font-size: 28px; margin-bottom: 5px; }
.boxing-glove { color: #000; }
.receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; } .receipt-title { font-size: 20px; font-weight: bold; margin-bottom: 3px; letter-spacing: 2px; }
.receipt-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator { .receipt-separator {
text-align: center; text-align: center;
margin: 12px 0; margin: 12px 0;
......
...@@ -164,16 +164,16 @@ let animationFrame = null; ...@@ -164,16 +164,16 @@ let animationFrame = null;
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Generate QR code for mobile access // Generate QR code for mobile access
generateMobileAccessQR(); generateMobileAccessQR();
// Initialize QR scanner elements // Initialize QR scanner elements
video = document.getElementById('qr-video'); video = document.getElementById('qr-video');
canvas = document.getElementById('qr-canvas'); canvas = document.getElementById('qr-canvas');
canvasContext = canvas.getContext('2d'); canvasContext = canvas.getContext('2d');
// Scanner controls // Scanner controls
document.getElementById('start-scanner').addEventListener('click', startScanner); document.getElementById('start-scanner').addEventListener('click', startScanner);
document.getElementById('stop-scanner').addEventListener('click', stopScanner); document.getElementById('stop-scanner').addEventListener('click', stopScanner);
// Barcode input handling // Barcode input handling
const barcodeInput = document.getElementById('barcode-input'); const barcodeInput = document.getElementById('barcode-input');
barcodeInput.addEventListener('input', handleBarcodeInput); barcodeInput.addEventListener('input', handleBarcodeInput);
...@@ -183,6 +183,11 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -183,6 +183,11 @@ document.addEventListener('DOMContentLoaded', function() {
processBarcodeInput(); processBarcodeInput();
} }
}); });
// Focus on barcode input immediately when page loads
setTimeout(() => {
barcodeInput.focus();
}, 100);
}); });
function generateMobileAccessQR() { function generateMobileAccessQR() {
......
...@@ -10,6 +10,42 @@ ...@@ -10,6 +10,42 @@
</div> </div>
</div> </div>
<!-- Keyboard Input Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-keyboard me-2"></i>Match Selection
</h5>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-hashtag"></i>
</span>
<input type="text" class="form-control form-control-lg" id="match-input"
placeholder="Enter match number and press Enter" readonly
style="font-size: 1.2rem; font-weight: bold;">
<span class="input-group-text">
<small class="text-muted">Press digits + Enter to select match</small>
</span>
</div>
</div>
<div class="col-md-4 text-end">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Use keyboard navigation for faster betting
</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Back Button and Submit Button --> <!-- Back Button and Submit Button -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
...@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() { document.getElementById('btn-submit-bet').addEventListener('click', function() {
submitBet(); submitBet();
}); });
// Initialize keyboard navigation
initializeKeyboardNavigation();
}); });
// Keyboard navigation state
let keyboardState = {
inputBuffer: '',
currentMatch: null,
currentOutcomeIndex: -1,
outcomes: []
};
function initializeKeyboardNavigation() {
const matchInput = document.getElementById('match-input');
// Global keyboard event listener
document.addEventListener('keydown', function(event) {
// Handle digit input for match selection
if (event.key >= '0' && event.key <= '9' && !isInInputField(event.target)) {
event.preventDefault();
keyboardState.inputBuffer += event.key;
matchInput.value = keyboardState.inputBuffer;
matchInput.classList.add('is-valid');
return;
}
// Handle Enter key for match selection
if (event.key === 'Enter' && keyboardState.inputBuffer && !isInInputField(event.target)) {
event.preventDefault();
const matchNumber = parseInt(keyboardState.inputBuffer);
selectMatchByNumber(matchNumber);
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Tab key for outcome navigation (only when in match)
if (event.key === 'Tab' && keyboardState.currentMatch) {
event.preventDefault();
navigateOutcomes();
return;
}
// Handle digits + Enter for bet amount (when outcome is selected)
if (event.key === 'Enter' && keyboardState.currentOutcomeIndex >= 0 && keyboardState.inputBuffer) {
event.preventDefault();
const amount = parseFloat(keyboardState.inputBuffer);
if (amount > 0) {
enterBetAmount(amount);
}
keyboardState.inputBuffer = '';
matchInput.value = '';
matchInput.classList.remove('is-valid');
return;
}
// Handle Ctrl+Enter for submitting bet
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
submitBet();
return;
}
// Handle Escape key for canceling
if (event.key === 'Escape') {
event.preventDefault();
if (keyboardState.currentMatch) {
// Close current match and reset
closeCurrentMatch();
} else {
// Go back to bets page
window.location.href = '/cashier/bets';
}
return;
}
});
}
function isInInputField(element) {
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' ||
element.contentEditable === 'true';
}
function selectMatchByNumber(matchNumber) {
// Find match card with this number
const matchCards = document.querySelectorAll('.match-card');
for (const card of matchCards) {
const matchId = card.getAttribute('data-match-id');
const headerText = card.querySelector('h6').textContent;
const matchNumMatch = headerText.match(/Match #(\d+)/);
if (matchNumMatch && parseInt(matchNumMatch[1]) === matchNumber) {
// Open this match
openMatch(card, matchId);
return;
}
}
// Match not found - visual feedback
const matchInput = document.getElementById('match-input');
matchInput.classList.add('is-invalid');
setTimeout(() => matchInput.classList.remove('is-invalid'), 1000);
}
function openMatch(card, matchId) {
// Close any currently open match
closeCurrentMatch();
// Open the selected match
const outcomesDiv = document.getElementById(`outcomes-${matchId}`);
const icon = card.querySelector('.toggle-match i');
if (outcomesDiv && outcomesDiv.style.display === 'none') {
outcomesDiv.style.display = 'block';
icon.className = 'fas fa-chevron-up me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-up me-1"></i>Hide Outcomes';
// Set as current match
keyboardState.currentMatch = matchId;
keyboardState.currentOutcomeIndex = -1;
// Get outcomes for navigation
const outcomeCards = outcomesDiv.querySelectorAll('.card');
keyboardState.outcomes = Array.from(outcomeCards);
// Highlight the match card
card.classList.add('border-primary');
card.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
}
}
function closeCurrentMatch() {
if (keyboardState.currentMatch) {
const card = document.querySelector(`[data-match-id="${keyboardState.currentMatch}"]`);
if (card) {
const outcomesDiv = document.getElementById(`outcomes-${keyboardState.currentMatch}`);
const icon = card.querySelector('.toggle-match i');
outcomesDiv.style.display = 'none';
icon.className = 'fas fa-chevron-down me-1';
card.querySelector('.toggle-match').innerHTML = '<i class="fas fa-chevron-down me-1"></i>Select Outcomes';
// Remove highlighting
card.classList.remove('border-primary');
card.style.boxShadow = '';
}
keyboardState.currentMatch = null;
keyboardState.currentOutcomeIndex = -1;
keyboardState.outcomes = [];
}
}
function navigateOutcomes() {
if (keyboardState.outcomes.length === 0) return;
// Remove previous highlighting
if (keyboardState.currentOutcomeIndex >= 0) {
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.border = '';
keyboardState.outcomes[keyboardState.currentOutcomeIndex].style.boxShadow = '';
}
// Move to next outcome
keyboardState.currentOutcomeIndex = (keyboardState.currentOutcomeIndex + 1) % keyboardState.outcomes.length;
// Highlight current outcome
const currentOutcome = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
currentOutcome.style.border = '3px solid #0d6efd';
currentOutcome.style.boxShadow = '0 0 0 0.2rem rgba(13, 110, 253, 0.25)';
// Scroll into view
currentOutcome.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
function enterBetAmount(amount) {
if (keyboardState.currentOutcomeIndex < 0 || !keyboardState.outcomes[keyboardState.currentOutcomeIndex]) return;
const outcomeCard = keyboardState.outcomes[keyboardState.currentOutcomeIndex];
const amountInput = outcomeCard.querySelector('.amount-input');
if (amountInput) {
amountInput.value = amount.toFixed(2);
amountInput.dispatchEvent(new Event('input')); // Trigger the input event to update summary
// Visual feedback
amountInput.classList.add('is-valid');
setTimeout(() => amountInput.classList.remove('is-valid'), 500);
// Move to next outcome automatically
setTimeout(() => navigateOutcomes(), 200);
}
}
let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] } let selectedOutcomes = new Map(); // matchId -> { outcomes: [], amounts: [] }
// Function to load and display available matches for betting // Function to load and display available matches for betting
......
...@@ -660,7 +660,7 @@ ...@@ -660,7 +660,7 @@
document.getElementById('introDropZone').classList.remove('drag-over'); document.getElementById('introDropZone').classList.remove('drag-over');
const templateName = e.dataTransfer.getData('text/plain'); const templateName = e.dataTransfer.getData('text/plain');
const defaultTime = document.getElementById('defaultShowTime').value || '00:30'; const defaultTime = document.getElementById('defaultShowTime').value || '00:15';
// Add template to intro list // Add template to intro list
const newTemplate = { const newTemplate = {
......
...@@ -33,6 +33,7 @@ netifaces>=0.11.0 ...@@ -33,6 +33,7 @@ netifaces>=0.11.0
# Video and image processing # Video and image processing
opencv-python>=4.5.0 opencv-python>=4.5.0
Pillow>=9.0.0 Pillow>=9.0.0
python-barcode[images]>=0.14.0
# Screen capture and streaming (optional dependencies) # Screen capture and streaming (optional dependencies)
ffmpeg-python>=0.2.0 ffmpeg-python>=0.2.0
......
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