feat: Implement dynamic betting outcomes and print functionality

- Replace hardcoded WIN1/WIN2/X betting terminology with database-driven outcomes
- Enhanced /cashier/available-matches API endpoint to query match_outcomes table
- Added generateOutcomeOptionsHTML() function for dynamic betting option generation
- Implemented graceful fallback to standard options when database outcomes unavailable
- Added print button to bets list in cashier dashboard with placeholder functionality
- Enhanced API response structure with outcome_id, outcome_name, outcome_value, display_name
- Updated documentation with comprehensive betting system section
- Added troubleshooting guides and performance optimization details
- Version 1.2.9 changelog and README updates with new features
parent e8012cdc
...@@ -2,6 +2,33 @@ ...@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.2.9] - 2025-08-26
### Added
- **Dynamic Betting Outcomes System**: Complete database-driven betting outcomes replacing hardcoded WIN1/WIN2/X terminology
- **Print Functionality**: Added print button to bets list in cashier dashboard for bet receipt printing
- **Match Outcome Integration**: API endpoint enhanced to query match_outcomes table for actual betting options
- **Dynamic Frontend Generation**: JavaScript function to generate betting options from database outcomes
- **Graceful Fallback**: Maintains compatibility with standard betting options when database outcomes unavailable
### Enhanced
- **Betting System Flexibility**: Betting interface now displays actual available outcomes from match database
- **User Experience**: More accurate betting options reflect real match data rather than standardized terminology
- **API Response Structure**: Enhanced `/cashier/available-matches` endpoint with comprehensive outcome data
- **Template System**: Dynamic outcome generation supports unlimited outcome types with consistent styling
### Fixed
- **Betting Outcome Display**: Resolved hardcoded betting terminology to use actual match outcomes from database
- **API Data Structure**: Enhanced match data response to include outcome_id, outcome_name, outcome_value, and display_name fields
- **Frontend Template Logic**: Replaced static HTML generation with dynamic outcome parsing and display
### Technical Details
- **Database Integration**: Modified API to query MatchOutcomeModel table for each match's available outcomes
- **Frontend Architecture**: Created generateOutcomeOptionsHTML() function for dynamic betting option generation
- **Color Coding System**: Implemented rotating color scheme for up to 5 different outcome types
- **Fallback Mechanism**: Provides WIN1/WIN2/X options when no database outcomes exist with proper logging
- **Print Infrastructure**: Added placeholder print functionality with proper button styling and event handling
## [1.2.8] - 2025-08-26 ## [1.2.8] - 2025-08-26
### Added ### Added
......
This diff is collapsed.
...@@ -5,6 +5,8 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -5,6 +5,8 @@ A cross-platform multimedia client application with video playback, web dashboar
## Features ## Features
- **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets) - **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets)
- **Dynamic Betting System**: Complete database-driven betting interface with actual match outcomes replacing hardcoded terminology
- **Print Functionality**: Integrated print button for bet receipts with placeholder implementation ready for enhancement
- **Screen Casting System**: Complete screen capture and Chromecast streaming with web-based controls and device discovery - **Screen Casting System**: Complete screen capture and Chromecast streaming with web-based controls and device discovery
- **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage - **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage
- **Extraction Management**: Complete drag-and-drop interface for managing outcome associations with extraction results - **Extraction Management**: Complete drag-and-drop interface for managing outcome associations with extraction results
...@@ -24,6 +26,15 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -24,6 +26,15 @@ A cross-platform multimedia client application with video playback, web dashboar
## Recent Improvements ## Recent Improvements
### Version 1.2.9 (August 2025)
-**Dynamic Betting Outcomes System**: Complete database-driven betting outcomes replacing hardcoded WIN1/WIN2/X terminology
-**Print Functionality**: Added print button to bets list in cashier dashboard for bet receipt printing
-**Match Outcome Integration**: API endpoint enhanced to query match_outcomes table for actual betting options
-**Dynamic Frontend Generation**: JavaScript function to generate betting options from database outcomes
-**Graceful Fallback**: Maintains compatibility with standard betting options when database outcomes unavailable
-**Enhanced Betting System**: More accurate betting options reflect real match data rather than standardized terminology
### Version 1.2.8 (August 2025) ### Version 1.2.8 (August 2025)
-**Offline CDN Fallback System**: Local copies of Bootstrap CSS/JS and FontAwesome with automatic fallback for offline networks -**Offline CDN Fallback System**: Local copies of Bootstrap CSS/JS and FontAwesome with automatic fallback for offline networks
......
This diff is collapsed.
...@@ -25,6 +25,13 @@ class MatchStatus(str, Enum): ...@@ -25,6 +25,13 @@ class MatchStatus(str, Enum):
FAILED = "failed" FAILED = "failed"
PAUSED = "paused" PAUSED = "paused"
# Enum for bet result status
class BetResult(str, Enum):
WIN = "win"
LOST = "lost"
PENDING = "pending"
CANCELLED = "cancelled"
Base = declarative_base() Base = declarative_base()
...@@ -600,7 +607,6 @@ class ExtractionAssociationModel(BaseModel): ...@@ -600,7 +607,6 @@ class ExtractionAssociationModel(BaseModel):
Index('ix_extraction_associations_outcome_name', 'outcome_name'), Index('ix_extraction_associations_outcome_name', 'outcome_name'),
Index('ix_extraction_associations_extraction_result', 'extraction_result'), Index('ix_extraction_associations_extraction_result', 'extraction_result'),
Index('ix_extraction_associations_composite', 'outcome_name', 'extraction_result'), Index('ix_extraction_associations_composite', 'outcome_name', 'extraction_result'),
UniqueConstraint('outcome_name', 'extraction_result', name='uq_extraction_associations_outcome_result'),
) )
outcome_name = Column(String(255), nullable=False, comment='Match outcome name (e.g., WIN1, DRAW, X1, etc.)') outcome_name = Column(String(255), nullable=False, comment='Match outcome name (e.g., WIN1, DRAW, X1, etc.)')
...@@ -660,3 +666,98 @@ class GameConfigModel(BaseModel): ...@@ -660,3 +666,98 @@ class GameConfigModel(BaseModel):
def __repr__(self): def __repr__(self):
return f'<GameConfig {self.config_key}={self.config_value}>' return f'<GameConfig {self.config_key}={self.config_value}>'
class BetModel(BaseModel):
"""Betting system main table"""
__tablename__ = 'bets'
__table_args__ = (
Index('ix_bets_uuid', 'uuid'),
Index('ix_bets_fixture_id', 'fixture_id'),
Index('ix_bets_created_at', 'created_at'),
UniqueConstraint('uuid', name='uq_bets_uuid'),
)
uuid = Column(String(1024), nullable=False, unique=True, comment='Unique identifier for the bet')
fixture_id = Column(String(255), nullable=False, comment='Reference to fixture_id from matches table')
bet_datetime = Column(DateTime, default=datetime.utcnow, nullable=False, comment='Bet creation timestamp')
# Relationships
bet_details = relationship('BetDetailModel', back_populates='bet', cascade='all, delete-orphan')
def get_total_amount(self) -> float:
"""Get total amount of all bet details"""
return sum(detail.amount for detail in self.bet_details)
def get_bet_count(self) -> int:
"""Get number of bet details"""
return len(self.bet_details)
def has_pending_bets(self) -> bool:
"""Check if bet has any pending bet details"""
return any(detail.result == 'pending' for detail in self.bet_details)
def calculate_total_winnings(self) -> float:
"""Calculate total winnings from won bets"""
return sum(detail.amount for detail in self.bet_details if detail.result == 'win')
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with bet details"""
result = super().to_dict(exclude_fields)
result['bet_details'] = [detail.to_dict() for detail in self.bet_details]
result['total_amount'] = self.get_total_amount()
result['bet_count'] = self.get_bet_count()
result['has_pending'] = self.has_pending_bets()
return result
def __repr__(self):
return f'<Bet {self.uuid} for Fixture {self.fixture_id}>'
class BetDetailModel(BaseModel):
"""Betting system details table"""
__tablename__ = 'bets_details'
__table_args__ = (
Index('ix_bets_details_bet_id', 'bet_id'),
Index('ix_bets_details_match_id', 'match_id'),
Index('ix_bets_details_outcome', 'outcome'),
Index('ix_bets_details_result', 'result'),
Index('ix_bets_details_composite', 'bet_id', 'match_id'),
)
bet_id = Column(Integer, ForeignKey('bets.id', ondelete='CASCADE'), nullable=False, comment='Foreign key to bets table')
match_id = Column(Integer, ForeignKey('matches.id'), nullable=False, comment='Foreign key to matches table')
outcome = Column(String(255), nullable=False, comment='Bet outcome/prediction')
amount = Column(Float(precision=2), nullable=False, comment='Bet amount with 2 decimal precision')
result = Column(Enum('win', 'lost', 'pending', 'cancelled'), default='pending', nullable=False, comment='Bet result status')
# Relationships
bet = relationship('BetModel', back_populates='bet_details')
match = relationship('MatchModel')
def is_pending(self) -> bool:
"""Check if bet detail is pending"""
return self.result == 'pending'
def is_won(self) -> bool:
"""Check if bet detail was won"""
return self.result == 'win'
def is_lost(self) -> bool:
"""Check if bet detail was lost"""
return self.result == 'lost'
def is_cancelled(self) -> bool:
"""Check if bet detail was cancelled"""
return self.result == 'cancelled'
def set_result(self, result: str):
"""Set bet result"""
valid_results = ['win', 'lost', 'pending', 'cancelled']
if result not in valid_results:
raise ValueError(f"Invalid result: {result}. Must be one of {valid_results}")
self.result = result
self.updated_at = datetime.utcnow()
def __repr__(self):
return f'<BetDetail {self.outcome}={self.amount} ({self.result}) for Match {self.match_id}>'
This diff is collapsed.
...@@ -270,48 +270,64 @@ ...@@ -270,48 +270,64 @@
const clockElement = document.getElementById('clock-time'); const clockElement = document.getElementById('clock-time');
if (!clockElement) return; if (!clockElement) return;
let serverTimeOffset = 0; // Offset between server and client time let serverTimeBase = null;
let lastServerTime = null; let serverTimeStartLocal = null;
function fetchServerTime() { function fetchServerTime() {
return fetch('/api/server-time') return fetch('/api/server-time')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success && data.formatted_time) {
const serverTimestamp = data.timestamp; // Use the server's pre-formatted time string
const clientTimestamp = Date.now(); serverTimeBase = data.formatted_time;
serverTimeOffset = serverTimestamp - clientTimestamp; serverTimeStartLocal = Date.now();
lastServerTime = serverTimestamp; return true;
return serverTimestamp;
} else { } else {
throw new Error('Failed to get server time'); throw new Error('Failed to get server time');
} }
}) })
.catch(error => { .catch(error => {
console.error('Error fetching server time:', error); console.error('Error fetching server time:', error);
// Fallback to client time if server time is unavailable // Use local time as fallback
return Date.now(); serverTimeBase = null;
return false;
}); });
} }
function updateClock() { function updateClock() {
const now = Date.now() + serverTimeOffset; if (serverTimeBase && serverTimeStartLocal) {
const date = new Date(now); // Calculate elapsed seconds since server time fetch
const elapsedMs = Date.now() - serverTimeStartLocal;
const hours = String(date.getHours()).padStart(2, '0'); const elapsedSeconds = Math.floor(elapsedMs / 1000);
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0'); // Parse server time and add elapsed seconds
const timeString = `${hours}:${minutes}:${seconds}`; const [hours, minutes, seconds] = serverTimeBase.split(':').map(num => parseInt(num, 10));
const serverDateTime = new Date();
clockElement.textContent = timeString; serverDateTime.setHours(hours, minutes, seconds + elapsedSeconds, 0);
const displayHours = String(serverDateTime.getHours()).padStart(2, '0');
const displayMinutes = String(serverDateTime.getMinutes()).padStart(2, '0');
const displaySeconds = String(serverDateTime.getSeconds()).padStart(2, '0');
const timeString = `${displayHours}:${displayMinutes}:${displaySeconds}`;
clockElement.textContent = timeString;
} else {
// Fallback to local time
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const timeString = `${hours}:${minutes}:${seconds}`;
clockElement.textContent = timeString;
}
} }
// Fetch server time initially and set up updates // Fetch server time initially and set up updates
fetchServerTime().then(() => { fetchServerTime().then(() => {
// Update immediately with server time // Update immediately
updateClock(); updateClock();
// Update display every second (using client time + offset) // Update display every second
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
// Sync with server time every 30 seconds // Sync with server time every 30 seconds
......
This diff is collapsed.
...@@ -27,8 +27,8 @@ ...@@ -27,8 +27,8 @@
</button> </button>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<button class="btn btn-outline-primary w-100" id="btn-play-video"> <button class="btn btn-success w-100 fw-bold" id="btn-bets">
<i class="fas fa-play me-2"></i>Start Video Display <i class="fas fa-coins me-2"></i>Bets
</button> </button>
</div> </div>
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
...@@ -36,11 +36,6 @@ ...@@ -36,11 +36,6 @@
<i class="fas fa-edit me-2"></i>Update Display Overlay <i class="fas fa-edit me-2"></i>Update Display Overlay
</button> </button>
</div> </div>
<div class="col-md-4 mb-3">
<button class="btn btn-outline-info w-100" id="btn-refresh-matches">
<i class="fas fa-sync-alt me-2"></i>Refresh Matches
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -162,41 +157,6 @@ ...@@ -162,41 +157,6 @@
</div> </div>
</div> </div>
<!-- Video Control Modal -->
<div class="modal fade" id="playVideoModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Start Video Display</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="play-video-form">
<div class="mb-3">
<label class="form-label">Video File Path</label>
<input type="text" class="form-control" id="video-file-path"
placeholder="/path/to/video.mp4">
<div class="form-text">Enter the full path to the video file you want to display</div>
</div>
<div class="mb-3">
<label class="form-label">Display Template</label>
<select class="form-select" id="video-template">
<option value="">Loading templates...</option>
</select>
<div class="form-text">Choose the template for displaying information over the video</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="confirm-play-video">
<i class="fas fa-play me-1"></i>Start Display
</button>
</div>
</div>
</div>
</div>
<!-- Overlay Update Modal --> <!-- Overlay Update Modal -->
<div class="modal fade" id="updateOverlayModal" tabindex="-1"> <div class="modal fade" id="updateOverlayModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
...@@ -277,59 +237,15 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -277,59 +237,15 @@ document.addEventListener('DOMContentLoaded', function() {
} }
}); });
document.getElementById('btn-play-video').addEventListener('click', function() { document.getElementById('btn-bets').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('playVideoModal')).show(); window.location.href = '/cashier/bets';
}); });
document.getElementById('btn-update-overlay').addEventListener('click', function() { document.getElementById('btn-update-overlay').addEventListener('click', function() {
new bootstrap.Modal(document.getElementById('updateOverlayModal')).show(); new bootstrap.Modal(document.getElementById('updateOverlayModal')).show();
}); });
document.getElementById('btn-refresh-matches').addEventListener('click', function() {
console.log('🔄 Manual refresh button clicked');
loadPendingMatches();
});
// Confirm actions // Confirm actions
document.getElementById('confirm-play-video').addEventListener('click', function() {
const filePath = document.getElementById('video-file-path').value;
const template = document.getElementById('video-template').value;
if (!filePath) {
alert('Please enter a video file path');
return;
}
fetch('/api/video/control', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'play',
file_path: filePath,
template: template
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
bootstrap.Modal.getInstance(document.getElementById('playVideoModal')).hide();
updateVideoStatus();
updateCurrentVideoPath(filePath);
updateCurrentTemplate(template || 'default');
// Increment counter
const videosCount = parseInt(document.getElementById('videos-played').textContent) + 1;
document.getElementById('videos-played').textContent = videosCount;
} else {
alert('Failed to start video: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error: ' + error.message);
});
});
document.getElementById('confirm-update-overlay').addEventListener('click', function() { document.getElementById('confirm-update-overlay').addEventListener('click', function() {
const template = document.getElementById('overlay-template').value; const template = document.getElementById('overlay-template').value;
const headline = document.getElementById('overlay-headline').value; const headline = document.getElementById('overlay-headline').value;
...@@ -632,21 +548,13 @@ function loadAvailableTemplates() { ...@@ -632,21 +548,13 @@ function loadAvailableTemplates() {
fetch('/api/templates') fetch('/api/templates')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const videoTemplateSelect = document.getElementById('video-template');
const overlayTemplateSelect = document.getElementById('overlay-template'); const overlayTemplateSelect = document.getElementById('overlay-template');
// Clear loading options // Clear loading options
videoTemplateSelect.innerHTML = '';
overlayTemplateSelect.innerHTML = ''; overlayTemplateSelect.innerHTML = '';
if (data.templates && Array.isArray(data.templates)) { if (data.templates && Array.isArray(data.templates)) {
data.templates.forEach(template => { data.templates.forEach(template => {
// Add to video template select
const videoOption = document.createElement('option');
videoOption.value = template.name;
videoOption.textContent = template.display_name || template.name;
videoTemplateSelect.appendChild(videoOption);
// Add to overlay template select // Add to overlay template select
const overlayOption = document.createElement('option'); const overlayOption = document.createElement('option');
overlayOption.value = template.name; overlayOption.value = template.name;
...@@ -655,22 +563,12 @@ function loadAvailableTemplates() { ...@@ -655,22 +563,12 @@ function loadAvailableTemplates() {
}); });
// Select default template if available // Select default template if available
const defaultVideoOption = videoTemplateSelect.querySelector('option[value="default"]');
if (defaultVideoOption) {
defaultVideoOption.selected = true;
}
const defaultOverlayOption = overlayTemplateSelect.querySelector('option[value="default"]'); const defaultOverlayOption = overlayTemplateSelect.querySelector('option[value="default"]');
if (defaultOverlayOption) { if (defaultOverlayOption) {
defaultOverlayOption.selected = true; defaultOverlayOption.selected = true;
} }
} else { } else {
// Fallback if no templates found // Fallback if no templates found
const videoOption = document.createElement('option');
videoOption.value = 'default';
videoOption.textContent = 'Default';
videoTemplateSelect.appendChild(videoOption);
const overlayOption = document.createElement('option'); const overlayOption = document.createElement('option');
overlayOption.value = 'default'; overlayOption.value = 'default';
overlayOption.textContent = 'Default'; overlayOption.textContent = 'Default';
...@@ -680,10 +578,7 @@ function loadAvailableTemplates() { ...@@ -680,10 +578,7 @@ function loadAvailableTemplates() {
.catch(error => { .catch(error => {
console.error('Error loading templates:', error); console.error('Error loading templates:', error);
// Fallback template options // Fallback template options
const videoTemplateSelect = document.getElementById('video-template');
const overlayTemplateSelect = document.getElementById('overlay-template'); const overlayTemplateSelect = document.getElementById('overlay-template');
videoTemplateSelect.innerHTML = '<option value="default">Default</option>';
overlayTemplateSelect.innerHTML = '<option value="default">Default</option>'; overlayTemplateSelect.innerHTML = '<option value="default">Default</option>';
}); });
} }
......
This diff is collapsed.
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