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]:
# Other dependencies
'packaging',
'pkg_resources',
# Barcode generation
'barcode',
'barcode.codex',
'barcode.ean',
'barcode.isbn',
'barcode.code39',
'barcode.code128',
'barcode.pzn',
]
# Conditionally add ffmpeg module if available
......
......@@ -316,7 +316,7 @@ class GeneralConfig:
app_name: str = "MbetterClient"
log_level: str = "INFO"
enable_qt: bool = True
match_interval: int = 20 # Default match interval in minutes
match_interval: int = 5 # Default match interval in minutes
@dataclass
......
......@@ -387,7 +387,7 @@ class MatchTimerComponent(ThreadedComponent):
try:
if self.config_manager:
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:
return 20 # Default fallback
except Exception as e:
......
......@@ -1046,9 +1046,9 @@ def get_match_interval():
try:
if api_bp.config_manager:
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:
match_interval = 20 # Default fallback
match_interval = 5 # Default fallback
return jsonify({
"success": True,
......@@ -1778,9 +1778,12 @@ def get_intro_templates():
# Default configuration
default_config = {
'templates': [],
'default_show_time': '00:30',
'rotating_time': '05:00'
'templates': [
{'name': 'fixtures', 'show_time': '00:15'},
{'name': 'match', 'show_time': '00:15'}
],
'default_show_time': '00:15',
'rotating_time': '00:15'
}
if intro_config:
......@@ -2390,9 +2393,9 @@ def get_match_timer_config():
try:
if api_bp.config_manager:
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:
match_interval = 20 # Default fallback
match_interval = 5 # Default fallback
return jsonify({
"success": True,
......
......@@ -429,13 +429,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = `
<div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon -->
<!-- Header -->
<div class="receipt-header">
<div class="receipt-logo">
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
<div class="receipt-title">BETTING SLIP</div>
</div>
<!-- Separator -->
......@@ -514,16 +510,9 @@ function generateReceiptHtml(betData) {
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- QR Code and Barcode -->
<div class="receipt-verification">
<div class="receipt-qr">
<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>
<!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<!-- QR Code and Barcode will be conditionally added here -->
</div>
<!-- Footer -->
......@@ -535,10 +524,9 @@ function generateReceiptHtml(betData) {
</div>
`;
// Generate QR code and barcode after inserting HTML
// Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => {
generateQRCode(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
generateVerificationCodes(betData.uuid);
}, 100);
return receiptHtml;
......@@ -554,30 +542,88 @@ function generateQRCode(betUuid) {
}
}
function generateBarcodeForReceipt(betUuid) {
// Generate barcode for thermal receipt if enabled
function generateVerificationCodes(betUuid) {
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}`)
.then(response => response.json())
.then(data => {
if (data.success && data.enabled && data.barcode_data) {
const barcodeData = data.barcode_data;
// Only show barcode if configured for thermal receipts
if (barcodeData.show_on_thermal && barcodeData.image_base64) {
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`);
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeContainer && barcodeElement) {
// 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;">`;
barcodeContainer.style.display = 'block';
}
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;">`;
}
}
})
.catch(error => {
console.warn('Failed to load barcode data:', error);
// Don't show error to user, just log it - barcodes are optional
console.warn('Failed to check barcode settings:', error);
});
}
......@@ -594,10 +640,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.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-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
......@@ -625,16 +668,13 @@ function printThermalReceipt() {
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.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-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
......
......@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() {
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
const printReceiptBtn = document.getElementById('btn-print-receipt');
if (printReceiptBtn) {
......@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = `
<div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon -->
<!-- Header -->
<div class="receipt-header">
<div class="receipt-logo">
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
<div class="receipt-title">BETTING SLIP</div>
</div>
<!-- Separator -->
......@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) {
<span>DATE:</span>
<span>${betData.bet_datetime}</span>
</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>
<!-- Separator -->
......@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span>
</div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
</div>
`;
......@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) {
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- QR Code and Barcode -->
<div class="receipt-verification">
<div class="receipt-qr">
<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>
<!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<!-- QR Code and Barcode will be conditionally added here -->
</div>
<!-- Footer -->
......@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) {
</div>
`;
// Generate QR code and barcode after inserting HTML
// Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => {
generateQRCode(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
generateVerificationCodes(betData.uuid);
}, 100);
return receiptHtml;
......@@ -673,30 +659,88 @@ function generateQRCode(betUuid) {
}
}
function generateBarcodeForReceipt(betUuid) {
// Generate barcode for thermal receipt if enabled
function generateVerificationCodes(betUuid) {
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}`)
.then(response => response.json())
.then(data => {
if (data.success && data.enabled && data.barcode_data) {
const barcodeData = data.barcode_data;
// Only show barcode if configured for thermal receipts
if (barcodeData.show_on_thermal && barcodeData.image_base64) {
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`);
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeContainer && barcodeElement) {
// 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;">`;
barcodeContainer.style.display = 'block';
}
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;">`;
}
}
})
.catch(error => {
console.warn('Failed to load barcode data:', error);
// Don't show error to user, just log it - barcodes are optional
console.warn('Failed to check barcode settings:', error);
});
}
......@@ -713,10 +757,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.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-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
......@@ -744,16 +785,13 @@ function printThermalReceipt() {
color: #000;
background: #fff;
}
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
.thermal-receipt-content {
max-width: 300px;
margin: 0 auto;
padding: 10px;
}
.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-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
......
......@@ -10,6 +10,42 @@
</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 -->
<div class="row mb-4">
<div class="col-12">
......@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() {
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: [] }
// Function to load and display available matches for betting
......
......@@ -354,7 +354,7 @@ document.addEventListener('currencySettingsLoaded', function(event) {
document.addEventListener('DOMContentLoaded', function() {
// Generate QR code for bet verification
generateBetVerificationQR();
// Cancel entire bet button
const cancelBetBtn = document.getElementById('btn-cancel-bet');
if (cancelBetBtn) {
......@@ -363,7 +363,7 @@ document.addEventListener('DOMContentLoaded', function() {
cancelEntireBet(betUuid);
});
}
// Delete bet detail buttons
const deleteDetailBtns = document.querySelectorAll('.btn-delete-detail');
deleteDetailBtns.forEach(function(btn) {
......@@ -372,7 +372,7 @@ document.addEventListener('DOMContentLoaded', function() {
deleteBetDetail(detailId);
});
});
// Preview bet button
const previewBetBtns = document.querySelectorAll('.btn-preview-bet');
previewBetBtns.forEach(function(btn) {
......@@ -390,7 +390,7 @@ document.addEventListener('DOMContentLoaded', function() {
directPrintBet(betId);
});
});
// Print receipt button in modal
const printReceiptBtn = document.getElementById('btn-print-receipt');
if (printReceiptBtn) {
......@@ -398,7 +398,7 @@ document.addEventListener('DOMContentLoaded', function() {
printThermalReceipt();
});
}
// Mark as paid button
const markPaidBtn = document.getElementById('btn-mark-paid');
if (markPaidBtn) {
......@@ -407,7 +407,7 @@ document.addEventListener('DOMContentLoaded', function() {
markBetAsPaid(betUuid);
});
}
// Update current time every second
setInterval(updateCurrentTime, 1000);
updateCurrentTime();
......@@ -475,7 +475,7 @@ function generateThermalReceipt(betId) {
const receiptContainer = document.getElementById('thermal-receipt');
const receiptHtml = generateReceiptHtml(window.betData);
receiptContainer.innerHTML = receiptHtml;
// Show the modal
const modal = new bootstrap.Modal(document.getElementById('printReceiptModal'));
modal.show();
......@@ -483,21 +483,17 @@ function generateThermalReceipt(betId) {
function generateReceiptHtml(betData) {
const currentDateTime = new Date().toLocaleString();
let receiptHtml = `
<div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon -->
<!-- Header -->
<div class="receipt-header">
<div class="receipt-logo">
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
<div class="receipt-title">BETTING SLIP</div>
</div>
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- Bet Information -->
<div class="receipt-info">
<div class="receipt-row">
......@@ -517,14 +513,14 @@ function generateReceiptHtml(betData) {
<span>${betData.bet_count}</span>
</div>
</div>
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- Bet Details -->
<div class="receipt-bets">
`;
let totalAmount = 0;
betData.bet_details.forEach((detail, index) => {
totalAmount += parseFloat(detail.amount);
......@@ -548,18 +544,18 @@ function generateReceiptHtml(betData) {
</div>
</div>
`;
if (index < betData.bet_details.length - 1) {
receiptHtml += `<div class="receipt-separator">- - - - - - - - - - - - - - - - -</div>`;
}
});
receiptHtml += `
</div>
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- Total -->
<div class="receipt-total">
<div class="receipt-total-line">
......@@ -567,22 +563,15 @@ function generateReceiptHtml(betData) {
<span>${formatCurrency(totalAmount)}</span>
</div>
</div>
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- QR Code and Barcode -->
<div class="receipt-verification">
<div class="receipt-qr">
<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>
<!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<!-- QR Code and Barcode will be conditionally added here -->
</div>
<!-- Footer -->
<div class="receipt-footer">
<div>Thank you for betting with MBetter!</div>
......@@ -591,80 +580,97 @@ function generateReceiptHtml(betData) {
</div>
</div>
`;
// Generate QR code and barcode after inserting HTML
// Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => {
generateQRCode(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
generateVerificationCodes(betData.uuid);
}, 100);
return receiptHtml;
}
function generateQRCode(betUuid) {
// Simple QR code generation using a free service
const qrContainer = document.getElementById(`qr-code-${betUuid}`);
if (qrContainer) {
// 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 generateVerificationCodes(betUuid) {
const verificationContainer = document.getElementById(`receipt-verification-${betUuid}`);
if (!verificationContainer) {
return;
}
}
function generateBarcodeForReceipt(betUuid) {
// Generate barcode for thermal receipt if enabled
console.log('🔍 BARCODE DEBUG: Starting barcode generation for bet:', betUuid);
fetch(`/api/barcode-data/${betUuid}`)
.then(response => {
console.log('🔍 BARCODE DEBUG: API response status:', response.status);
return response.json();
})
.then(data => {
console.log('🔍 BARCODE DEBUG: API response data:', data);
if (data.success && data.enabled && data.barcode_data) {
const barcodeData = data.barcode_data;
console.log('🔍 BARCODE DEBUG: Barcode data received:', barcodeData);
// Only show barcode if configured for thermal receipts
if (barcodeData.show_on_thermal && barcodeData.image_base64) {
console.log('🔍 BARCODE DEBUG: Thermal receipt barcode enabled, searching for DOM elements...');
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`);
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
console.log('🔍 BARCODE DEBUG: DOM elements found:', {
container: !!barcodeContainer,
element: !!barcodeElement,
containerDisplay: barcodeContainer ? barcodeContainer.style.display : 'not found'
});
if (barcodeContainer && barcodeElement) {
// Display the barcode image with exact configured dimensions
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';
console.log('🔍 BARCODE DEBUG: ✅ Barcode displayed successfully!');
} else {
console.warn('🔍 BARCODE DEBUG: ❌ DOM elements not found for barcode display');
// 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.warn('🔍 BARCODE DEBUG: ❌ Barcode not configured for thermal receipts or no image data:', {
show_on_thermal: barcodeData.show_on_thermal,
has_image: !!barcodeData.image_base64
});
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}`)
.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 => {
console.error('🔍 BARCODE DEBUG: ❌ API call failed:', error);
// Don't show error to user, just log it - barcodes are optional
console.warn('Failed to check barcode settings:', error);
});
}
......@@ -681,10 +687,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.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-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
......@@ -718,10 +721,7 @@ function printThermalReceipt() {
padding: 10px;
}
.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-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
......
......@@ -241,6 +241,15 @@ document.addEventListener('DOMContentLoaded', function() {
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)
function updateVideoStatus() {
fetch('/api/video/status')
......@@ -548,13 +557,9 @@ function generateReceiptHtml(betData) {
let receiptHtml = `
<div class="thermal-receipt-content">
<!-- Header with Boxing Glove Icon -->
<!-- Header -->
<div class="receipt-header">
<div class="receipt-logo">
<i class="fas fa-hand-rock boxing-glove"></i>
</div>
<div class="receipt-title">MBETTER</div>
<div class="receipt-subtitle">BETTING SLIP</div>
<div class="receipt-title">BETTING SLIP</div>
</div>
<!-- Separator -->
......@@ -570,14 +575,6 @@ function generateReceiptHtml(betData) {
<span>DATE:</span>
<span>${betData.bet_datetime}</span>
</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>
<!-- Separator -->
......@@ -605,9 +602,6 @@ function generateReceiptHtml(betData) {
<span>OUTCOME: ${detail.outcome}</span>
<span>${formatCurrency(parseFloat(detail.amount))}</span>
</div>
<div class="receipt-status">
STATUS: ${detail.result.toUpperCase()}
</div>
</div>
`;
......@@ -633,16 +627,9 @@ function generateReceiptHtml(betData) {
<!-- Separator -->
<div class="receipt-separator">================================</div>
<!-- QR Code and Barcode -->
<div class="receipt-verification">
<div class="receipt-qr">
<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>
<!-- QR Code and Barcode (conditional based on settings) -->
<div class="receipt-verification" id="receipt-verification-${betData.uuid}">
<!-- QR Code and Barcode will be conditionally added here -->
</div>
<!-- Footer -->
......@@ -654,10 +641,9 @@ function generateReceiptHtml(betData) {
</div>
`;
// Generate QR code and barcode after inserting HTML
// Generate QR code and barcode after inserting HTML (conditional)
setTimeout(() => {
generateQRCode(betData.uuid);
generateBarcodeForReceipt(betData.uuid);
generateVerificationCodes(betData.uuid);
}, 100);
return receiptHtml;
......@@ -673,30 +659,88 @@ function generateQRCode(betUuid) {
}
}
function generateBarcodeForReceipt(betUuid) {
// Generate barcode for thermal receipt if enabled
function generateVerificationCodes(betUuid) {
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}`)
.then(response => response.json())
.then(data => {
if (data.success && data.enabled && data.barcode_data) {
const barcodeData = data.barcode_data;
// Only show barcode if configured for thermal receipts
if (barcodeData.show_on_thermal && barcodeData.image_base64) {
const barcodeContainer = document.getElementById(`barcode-container-${betUuid}`);
const barcodeElement = document.getElementById(`barcode-${betUuid}`);
if (barcodeContainer && barcodeElement) {
// 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;">`;
barcodeContainer.style.display = 'block';
}
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;">`;
}
}
})
.catch(error => {
console.warn('Failed to load barcode data:', error);
// Don't show error to user, just log it - barcodes are optional
console.warn('Failed to check barcode settings:', error);
});
}
......@@ -713,10 +757,7 @@ function printThermalReceipt() {
body { margin: 0; padding: 10px; font-family: 'Courier New', monospace; }
.thermal-receipt-content { width: 100%; }
.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-subtitle { font-size: 12px; margin-bottom: 5px; }
.receipt-separator { text-align: center; margin: 8px 0; font-size: 10px; }
.receipt-info, .receipt-bets, .receipt-total, .receipt-footer { margin: 10px 0; }
.receipt-row, .receipt-bet-line, .receipt-total-line {
......@@ -750,10 +791,7 @@ function printThermalReceipt() {
padding: 10px;
}
.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-subtitle { font-size: 14px; margin-bottom: 5px; }
.receipt-separator {
text-align: center;
margin: 12px 0;
......
......@@ -164,16 +164,16 @@ let animationFrame = null;
document.addEventListener('DOMContentLoaded', function() {
// Generate QR code for mobile access
generateMobileAccessQR();
// Initialize QR scanner elements
video = document.getElementById('qr-video');
canvas = document.getElementById('qr-canvas');
canvasContext = canvas.getContext('2d');
// Scanner controls
document.getElementById('start-scanner').addEventListener('click', startScanner);
document.getElementById('stop-scanner').addEventListener('click', stopScanner);
// Barcode input handling
const barcodeInput = document.getElementById('barcode-input');
barcodeInput.addEventListener('input', handleBarcodeInput);
......@@ -183,6 +183,11 @@ document.addEventListener('DOMContentLoaded', function() {
processBarcodeInput();
}
});
// Focus on barcode input immediately when page loads
setTimeout(() => {
barcodeInput.focus();
}, 100);
});
function generateMobileAccessQR() {
......
......@@ -10,6 +10,42 @@
</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 -->
<div class="row mb-4">
<div class="col-12">
......@@ -171,8 +207,200 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-submit-bet').addEventListener('click', function() {
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: [] }
// Function to load and display available matches for betting
......
......@@ -660,7 +660,7 @@
document.getElementById('introDropZone').classList.remove('drag-over');
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
const newTemplate = {
......
......@@ -33,6 +33,7 @@ netifaces>=0.11.0
# Video and image processing
opencv-python>=4.5.0
Pillow>=9.0.0
python-barcode[images]>=0.14.0
# Screen capture and streaming (optional dependencies)
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