Update extraction cap calculation

parent a4fd9ee8
......@@ -234,6 +234,11 @@ def collect_hidden_imports() -> List[str]:
'barcode.codex',
'barcode.ean',
'barcode.isxn',
# Excel file generation
'openpyxl',
'openpyxl.styles',
'openpyxl.utils',
]
# Conditionally add ffmpeg module if available
......
......@@ -202,10 +202,11 @@ class SportsResponseHandler(ResponseHandler):
class UpdatesResponseHandler(ResponseHandler):
"""Response handler for /api/updates endpoint - synchronizes match data"""
def __init__(self, db_manager, user_data_dir, api_client=None):
def __init__(self, db_manager, user_data_dir, api_client=None, message_bus=None):
self.db_manager = db_manager
self.user_data_dir = user_data_dir
self.api_client = api_client # Reference to parent API client for token access
self.message_bus = message_bus # Reference to message bus for progress updates
self.zip_storage_dir = Path(user_data_dir) / "zip_files"
self.zip_storage_dir.mkdir(parents=True, exist_ok=True)
......@@ -289,6 +290,9 @@ class UpdatesResponseHandler(ResponseHandler):
logger.debug(f"Starting ZIP file downloads for {len(fixtures)} fixtures")
total_matches_with_zips = 0
# Send initial progress update - starting downloads
self._send_download_progress(0, processed_data['expected_zips'], "Starting downloads...")
for fixture_data in fixtures:
fixture_id = fixture_data.get('fixture_id', 'unknown')
matches = fixture_data.get('matches', [])
......@@ -310,10 +314,20 @@ class UpdatesResponseHandler(ResponseHandler):
match_data['zip_url'] = match_data['zip_download_url']
logger.debug(f"Found ZIP file to download: {zip_filename} for match {match_number} in fixture {fixture_id}")
# Send progress update - starting individual download
progress_percent = int((processed_data['downloaded_zips'] / max(1, processed_data['expected_zips'])) * 100)
self._send_download_progress(processed_data['downloaded_zips'], processed_data['expected_zips'],
f"Downloading {zip_filename}...")
download_success = self._download_zip_file(match_data)
if download_success:
processed_data['downloaded_zips'] += 1
logger.debug(f"Successfully downloaded ZIP file: {zip_filename} for match {match_number}")
# Send progress update - download completed
progress_percent = int((processed_data['downloaded_zips'] / max(1, processed_data['expected_zips'])) * 100)
self._send_download_progress(processed_data['downloaded_zips'], processed_data['expected_zips'],
f"Downloaded {zip_filename}")
elif 'zip_download_url' in match_data:
logger.debug(f"ZIP file download skipped or failed: {zip_filename} for match {match_number}")
......@@ -336,6 +350,13 @@ class UpdatesResponseHandler(ResponseHandler):
logger.debug(f"ZIP download summary: {processed_data['downloaded_zips']}/{processed_data['expected_zips']} ZIP files downloaded successfully from {total_matches_with_zips} matches")
# Send final progress update - downloads completed
if processed_data['expected_zips'] > 0:
self._send_download_progress(processed_data['downloaded_zips'], processed_data['expected_zips'],
f"Downloads completed - {processed_data['downloaded_zips']}/{processed_data['expected_zips']} files")
else:
self._send_download_progress(0, 0, "No downloads needed")
logger.info(f"Synchronized {processed_data['synchronized_matches']} matches, downloaded {processed_data['downloaded_zips']} ZIP files")
return processed_data
......@@ -592,6 +613,31 @@ class UpdatesResponseHandler(ResponseHandler):
logger.error(f"Failed to validate ZIP file {zip_path}: {e}")
return False
def _send_download_progress(self, downloaded: int, total: int, message: str):
"""Send download progress update via message bus"""
try:
if self.message_bus:
from ..core.message_bus import Message, MessageType
progress_data = {
'downloaded': downloaded,
'total': total,
'percentage': int((downloaded / max(1, total)) * 100) if total > 0 else 0,
'message': message,
'timestamp': time.time()
}
progress_message = Message(
type=MessageType.CUSTOM,
sender='api_client',
data={
'download_progress': progress_data
}
)
self.message_bus.publish(progress_message)
logger.debug(f"Sent download progress: {downloaded}/{total} - {message}")
except Exception as e:
logger.error(f"Failed to send download progress: {e}")
def _validate_existing_fixtures(self):
"""Validate existing fixtures on startup and remove invalid ones"""
try:
......@@ -719,7 +765,7 @@ class APIClient(ThreadedComponent):
'default': DefaultResponseHandler(),
'news': NewsResponseHandler(),
'sports': SportsResponseHandler(),
'updates': UpdatesResponseHandler(self.db_manager, get_user_data_dir(), self)
'updates': UpdatesResponseHandler(self.db_manager, get_user_data_dir(), self, self.message_bus)
}
# Statistics
......
......@@ -2361,26 +2361,50 @@ class GamesThread(ThreadedComponent):
total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0
logger.info(f"💵 [EXTRACTION DEBUG] Total bet amount calculated: {total_bet_amount:.2f} from {len(all_bets)} bets")
# Step 5: Get redistribution CAP
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Retrieving redistribution CAP")
# Step 5: Get UNDER/OVER result and calculate payouts for CAP adjustment
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Getting UNDER/OVER result and calculating payouts for CAP adjustment")
# Get the stored UNDER/OVER result from the match
match = session.query(MatchModel).filter_by(id=match_id).first()
under_over_result = match.under_over_result if match else None
logger.info(f"🎯 [EXTRACTION DEBUG] Stored under_over_result: '{under_over_result}'")
# Calculate UNDER/OVER payouts
under_payout = self._calculate_payout(match_id, 'UNDER', under_coeff, session)
over_payout = self._calculate_payout(match_id, 'OVER', over_coeff, session)
logger.info(f"🎯 [EXTRACTION DEBUG] UNDER payout: {under_payout:.2f}, OVER payout: {over_payout:.2f}")
# Get redistribution CAP
cap_percentage = self._get_redistribution_cap()
# Step 5.1: Get accumulated shortfall from previous extractions for today
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5.1: Retrieving accumulated shortfall for today")
# Get accumulated shortfall from previous extractions for today
today = datetime.now().date()
accumulated_shortfall = self._get_daily_shortfall(today, session)
logger.info(f"🎯 [EXTRACTION DEBUG] Accumulated shortfall: {accumulated_shortfall:.2f}")
# Step 5.2: Calculate CAP threshold including shortfall
# Calculate base CAP threshold
base_cap_threshold = total_bet_amount * (cap_percentage / 100.0)
# Adjust CAP threshold based on UNDER/OVER result
cap_threshold = base_cap_threshold + accumulated_shortfall
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, base threshold: {base_cap_threshold:.2f}, shortfall: {accumulated_shortfall:.2f}, final threshold: {cap_threshold:.2f}")
# If UNDER/OVER wins, subtract the winning payout from CAP threshold
if under_over_result == 'UNDER':
cap_threshold -= under_payout
logger.info(f"🎯 [EXTRACTION DEBUG] UNDER wins - adjusted CAP threshold: {cap_threshold:.2f} (subtracted {under_payout:.2f})")
elif under_over_result == 'OVER':
cap_threshold -= over_payout
logger.info(f"🎯 [EXTRACTION DEBUG] OVER wins - adjusted CAP threshold: {cap_threshold:.2f} (subtracted {over_payout:.2f})")
else:
logger.info(f"🎯 [EXTRACTION DEBUG] No UNDER/OVER winner - CAP threshold: {cap_threshold:.2f}")
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, base threshold: {base_cap_threshold:.2f}, final threshold: {cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Extraction summary - {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Payouts: {payouts}")
# Step 6: Filter payouts below CAP threshold
logger.info(f"🎯 [EXTRACTION DEBUG] Step 6: Filtering payouts below CAP threshold")
# Step 6: Filter payouts below adjusted CAP threshold
logger.info(f"🎯 [EXTRACTION DEBUG] Step 6: Filtering payouts below adjusted CAP threshold")
eligible_payouts = {k: v for k, v in payouts.items() if v <= cap_threshold}
logger.info(f"🎯 [EXTRACTION DEBUG] Eligible payouts (≤ {cap_threshold:.2f}): {eligible_payouts}")
......@@ -2564,11 +2588,40 @@ class GamesThread(ThreadedComponent):
bet.set_result('win', win_amount)
logger.info(f"DEBUG _update_bet_results: Set bet {bet.id} to win with amount {win_amount}")
# Update bets for associated winning outcomes to 'win'
if extraction_winning_outcome_names:
logger.info(f"DEBUG _update_bet_results: Updating bets for {len(extraction_winning_outcome_names)} associated winning outcomes: {extraction_winning_outcome_names}")
for outcome_name in extraction_winning_outcome_names:
# Skip if this outcome is already handled above (selected_result)
if outcome_name == selected_result:
continue
# Get coefficient for this associated outcome
associated_coefficient = self._get_outcome_coefficient(match_id, outcome_name, session)
associated_winning_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == outcome_name,
BetDetailModel.result == 'pending'
).all()
logger.info(f"DEBUG _update_bet_results: Found {len(associated_winning_bets)} winning {outcome_name} bets (associated)")
for bet in associated_winning_bets:
win_amount = bet.amount * associated_coefficient
bet.set_result('win', win_amount)
logger.info(f"DEBUG _update_bet_results: Set associated bet {bet.id} ({outcome_name}) to win with amount {win_amount}")
# Update all other bets to 'lost'
losing_outcomes = [selected_result, 'UNDER', 'OVER']
if extraction_winning_outcome_names:
losing_outcomes.extend(extraction_winning_outcome_names)
losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending',
~BetDetailModel.outcome.in_([selected_result, 'UNDER', 'OVER'])
~BetDetailModel.outcome.in_(losing_outcomes)
).update({'result': 'lost'})
logger.info(f"DEBUG _update_bet_results: Set {losing_count} other bets to lost")
......
......@@ -585,6 +585,7 @@ class WebDashboard(ThreadedComponent):
response = message.data.get("response")
timer_update = message.data.get("timer_update")
fixture_status_update = message.data.get("fixture_status_update")
download_progress = message.data.get("download_progress")
if response == "timer_state":
# Update stored timer state
......@@ -619,9 +620,17 @@ class WebDashboard(ThreadedComponent):
# Add fixture status update to notification queue for long-polling clients
self._add_client_notification("FIXTURE_STATUS_UPDATE", fixture_status_update, message.timestamp)
elif download_progress:
# Handle download progress updates from API client
if self.socketio:
# Broadcast download progress to all connected clients
self.socketio.emit('download_progress', download_progress)
logger.debug(f"Broadcasted download progress: {download_progress}")
except Exception as e:
logger.error(f"Failed to handle custom message: {e}")
def _handle_client_notification(self, message: Message):
"""Handle messages that should be sent to long-polling clients"""
try:
......
......@@ -27,6 +27,59 @@ def conditional_auth_decorator(condition, auth_decorator, fallback_decorator=log
return decorator
def is_bet_detail_winning(detail, match, session):
"""
Determine if a bet detail is winning based on direct result, extraction associations,
winning outcomes array, and under/over results.
Args:
detail: BetDetailModel instance
match: MatchModel instance
session: Database session
Returns:
bool: True if the bet detail is winning
"""
# Direct win check
if detail.result in ['won', 'win']:
return True
# Check if match has extraction data
if match:
# Check winning_outcomes array (contains all winning outcomes from extraction)
if match.winning_outcomes:
try:
if isinstance(match.winning_outcomes, str):
winning_outcomes = json.loads(match.winning_outcomes)
elif isinstance(match.winning_outcomes, list):
winning_outcomes = match.winning_outcomes
else:
winning_outcomes = []
if detail.outcome in winning_outcomes:
return True
except (json.JSONDecodeError, TypeError):
pass
# Check under_over_result
if match.under_over_result and detail.outcome == match.under_over_result:
return True
# Check extraction associations if match has extraction result
if match.result:
from ..database.models import ExtractionAssociationModel
# Check if this outcome is associated with the extraction result
association = session.query(ExtractionAssociationModel).filter_by(
outcome_name=detail.outcome,
extraction_result=match.result
).first()
if association:
return True
return False
def get_api_auth_decorator(require_admin=False):
"""Get API auth decorator that works with lazy initialization"""
def decorator(func):
......@@ -271,7 +324,7 @@ def bet_details(bet_id):
if detail.result == 'pending':
results['pending'] += 1
has_pending = True
elif detail.result in ['won', 'win']:
elif is_bet_detail_winning(detail, match, session):
results['won'] += 1
results['winnings'] += float(detail.amount) * float(odds) # Use actual odds
elif detail.result == 'lost':
......@@ -730,7 +783,7 @@ def cashier_bet_details(bet_id):
if detail.result == 'pending':
results['pending'] += 1
has_pending = True
elif detail.result in ['won', 'win']:
elif is_bet_detail_winning(detail, match, session):
results['won'] += 1
results['winnings'] += float(detail.amount) * float(odds) # Use actual odds
elif detail.result == 'lost':
......@@ -894,6 +947,50 @@ def change_password():
return render_template('auth/change_password.html')
@main_bp.route('/reports')
@login_required
def reports():
"""Reports page for admin/user"""
try:
# Get today's date for the date picker default
from datetime import date
today_date = date.today().isoformat()
return render_template('dashboard/reports.html',
user=current_user,
today_date=today_date,
page_title="Reports")
except Exception as e:
logger.error(f"Reports page error: {e}")
flash("Error loading reports page", "error")
return render_template('errors/500.html'), 500
@main_bp.route('/cashier/reports')
@login_required
def cashier_reports():
"""Reports page for cashier"""
try:
# Verify user is cashier
if not (hasattr(current_user, 'role') and current_user.role == 'cashier'):
if not (hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user()):
flash("Cashier access required", "error")
return redirect(url_for('main.index'))
# Get today's date for the date picker default
from datetime import date
today_date = date.today().isoformat()
return render_template('dashboard/reports.html',
user=current_user,
today_date=today_date,
page_title="Reports")
except Exception as e:
logger.error(f"Cashier reports page error: {e}")
flash("Error loading reports page", "error")
return render_template('errors/500.html'), 500
@main_bp.route('/statistics')
@login_required
def statistics():
......@@ -4560,7 +4657,7 @@ def get_cashier_bet_details(bet_id):
for detail in bet_details:
if detail.result == 'pending':
results['pending'] += 1
elif detail.result in ['won', 'win']:
elif is_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
results['won'] += 1
# Get odds for this outcome
odds = 0.0
......@@ -5001,7 +5098,7 @@ def verify_barcode():
for detail in bet_details:
if detail.result == 'pending':
results['pending'] += 1
elif detail.result in ['won', 'win']:
elif is_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
results['won'] += 1
# Get odds for this outcome
odds = 0.0
......@@ -5300,6 +5397,464 @@ def set_qrcode_settings():
return jsonify({"error": str(e)}), 500
# Reports API endpoints
@api_bp.route('/reports/daily-summary')
@get_api_auth_decorator()
def get_daily_reports_summary():
"""Get daily reports summary for a specific date"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel, ExtractionStatsModel
from datetime import datetime, date, timezone, timedelta
# Get date parameter (default to today)
date_param = request.args.get('date')
if date_param:
try:
target_date = datetime.strptime(date_param, '%Y-%m-%d').date()
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400
else:
target_date = date.today()
session = api_bp.db_manager.get_session()
try:
# Create the date range in UTC directly (same logic as cashier bets)
utc_offset = timedelta(hours=2) # User is in UTC+2
local_start = datetime.combine(target_date, datetime.min.time())
local_end = datetime.combine(target_date, datetime.max.time())
start_datetime = (local_start - utc_offset)
end_datetime = (local_end - utc_offset)
logger.info(f"Querying daily summary for local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Get all bets for the target date
bets_query = session.query(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
)
bets = bets_query.all()
logger.info(f"Found {len(bets)} bets for daily summary")
# Calculate totals
total_payin = 0.0
total_bets = 0
for bet in bets:
# Get bet details for this bet
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
bet_total = sum(float(detail.amount) for detail in bet_details)
total_payin += bet_total
total_bets += len(bet_details)
# Calculate total payout from extraction stats for the day
extraction_stats = session.query(ExtractionStatsModel).filter(
ExtractionStatsModel.match_datetime >= start_datetime,
ExtractionStatsModel.match_datetime <= end_datetime
).all()
total_payout = sum(float(stat.total_redistributed) for stat in extraction_stats)
total_matches = len(extraction_stats)
summary = {
'date': target_date.isoformat(),
'total_payin': float(total_payin),
'total_payout': float(total_payout),
'net_profit': float(total_payin - total_payout),
'total_bets': total_bets,
'total_matches': total_matches
}
return jsonify({
"success": True,
"summary": summary
})
finally:
session.close()
except Exception as e:
logger.error(f"API get daily reports summary error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/reports/match-reports')
@get_api_auth_decorator()
def get_match_reports():
"""Get match reports for a specific date"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel, ExtractionStatsModel
from datetime import datetime, date, timezone, timedelta
# Get date parameter (default to today)
date_param = request.args.get('date')
if date_param:
try:
target_date = datetime.strptime(date_param, '%Y-%m-%d').date()
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400
else:
target_date = date.today()
session = api_bp.db_manager.get_session()
try:
# Create the date range in UTC directly (same logic as cashier bets)
utc_offset = timedelta(hours=2) # User is in UTC+2
local_start = datetime.combine(target_date, datetime.min.time())
local_end = datetime.combine(target_date, datetime.max.time())
start_datetime = (local_start - utc_offset)
end_datetime = (local_end - utc_offset)
logger.info(f"Querying match reports for local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Get all matches that had bets on this day
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
)
# Group by match_id and calculate statistics
match_stats = {}
bet_details = bet_details_query.all()
for detail in bet_details:
match_id = detail.match_id
if match_id not in match_stats:
match_stats[match_id] = {
'match_id': match_id,
'bets_count': 0,
'payin': 0.0,
'payout': 0.0
}
match_stats[match_id]['bets_count'] += 1
match_stats[match_id]['payin'] += float(detail.amount)
# Calculate potential payout if bet was won
if detail.result in ['won', 'win']:
# Get match to find odds
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
match_stats[match_id]['payout'] += float(detail.amount) * float(odds)
# Get match information and merge with stats
matches_data = []
for match_id, stats in match_stats.items():
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
match_data = {
'match_id': match_id,
'match_number': match.match_number,
'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township,
'status': match.status,
'bets_count': stats['bets_count'],
'payin': stats['payin'],
'payout': stats['payout'],
'net': stats['payin'] - stats['payout']
}
matches_data.append(match_data)
# Sort by match number
matches_data.sort(key=lambda x: x.get('match_number', 0))
return jsonify({
"success": True,
"matches": matches_data,
"total": len(matches_data),
"date": target_date.isoformat()
})
finally:
session.close()
except Exception as e:
logger.error(f"API get match reports error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/reports/match-details')
@get_api_auth_decorator()
def get_match_details():
"""Get detailed bets for a specific match on a specific date"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel
from datetime import datetime, date, timezone, timedelta
# Get parameters
match_id = request.args.get('match_id')
date_param = request.args.get('date')
if not match_id:
return jsonify({"error": "match_id parameter is required"}), 400
if date_param:
try:
target_date = datetime.strptime(date_param, '%Y-%m-%d').date()
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400
else:
target_date = date.today()
session = api_bp.db_manager.get_session()
try:
# Create the date range in UTC directly (same logic as cashier bets)
utc_offset = timedelta(hours=2) # User is in UTC+2
local_start = datetime.combine(target_date, datetime.min.time())
local_end = datetime.combine(target_date, datetime.max.time())
start_datetime = (local_start - utc_offset)
end_datetime = (local_end - utc_offset)
logger.info(f"Querying match details for match {match_id}, local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Get bet details for this match on this date
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
BetDetailModel.match_id == match_id,
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
).order_by(BetModel.bet_datetime.desc())
bet_details = bet_details_query.all()
logger.info(f"Found {len(bet_details)} bet details for match {match_id}")
# Get match information
match = session.query(MatchModel).filter_by(id=match_id).first()
# Convert to response format
bets_data = []
for detail in bet_details:
# Get the bet for datetime
bet = session.query(BetModel).filter_by(uuid=detail.bet_id).first()
# Calculate potential winning
potential_winning = 0.0
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
potential_winning = float(detail.amount) * float(odds)
bet_data = {
'bet_id': detail.bet_id,
'bet_datetime': bet.bet_datetime.isoformat() if bet else None,
'outcome': detail.outcome,
'amount': float(detail.amount),
'result': detail.result,
'potential_winning': potential_winning
}
bets_data.append(bet_data)
return jsonify({
"success": True,
"match": {
'match_id': match_id,
'match_number': match.match_number if match else None,
'fighter1_township': match.fighter1_township if match else None,
'fighter2_township': match.fighter2_township if match else None,
'venue_kampala_township': match.venue_kampala_township if match else None
} if match else None,
"bets": bets_data,
"total": len(bets_data),
"date": target_date.isoformat()
})
finally:
session.close()
except Exception as e:
logger.error(f"API get match details error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/reports/download-excel')
@get_api_auth_decorator()
def download_excel_report():
"""Generate and download Excel report for a specific date"""
try:
from ..database.models import BetModel, BetDetailModel, MatchModel, ExtractionStatsModel
from datetime import datetime, date, timezone, timedelta
from flask import send_file
import io
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
# Get date parameter
date_param = request.args.get('date')
if date_param:
try:
target_date = datetime.strptime(date_param, '%Y-%m-%d').date()
except ValueError:
return jsonify({"error": "Invalid date format. Use YYYY-MM-DD"}), 400
else:
target_date = date.today()
session = api_bp.db_manager.get_session()
try:
# Create the date range in UTC directly (same logic as other report endpoints)
utc_offset = timedelta(hours=2) # User is in UTC+2
local_start = datetime.combine(target_date, datetime.min.time())
local_end = datetime.combine(target_date, datetime.max.time())
start_datetime = (local_start - utc_offset)
end_datetime = (local_end - utc_offset)
logger.info(f"Generating Excel report for local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Create workbook
wb = Workbook()
ws_summary = wb.active
ws_summary.title = "Daily Summary"
ws_matches = wb.create_sheet("Match Reports")
# Define styles
header_font = Font(bold=True, color="FFFFFF")
header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid")
border = Border(left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin'))
# Generate Daily Summary sheet
ws_summary['A1'] = f"Daily Report - {target_date.strftime('%Y-%m-%d')}"
ws_summary['A1'].font = Font(bold=True, size=16)
ws_summary.merge_cells('A1:E1')
# Get daily summary data
bets_query = session.query(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
)
bets = bets_query.all()
total_payin = 0.0
total_bets = 0
for bet in bets:
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
bet_total = sum(float(detail.amount) for detail in bet_details)
total_payin += bet_total
total_bets += len(bet_details)
extraction_stats = session.query(ExtractionStatsModel).filter(
ExtractionStatsModel.match_datetime >= start_datetime,
ExtractionStatsModel.match_datetime <= end_datetime
).all()
total_payout = sum(float(stat.total_redistributed) for stat in extraction_stats)
# Summary headers
ws_summary['A3'] = "Metric"
ws_summary['B3'] = "Value"
ws_summary['A3'].font = header_font
ws_summary['B3'].font = header_font
ws_summary['A3'].fill = header_fill
ws_summary['B3'].fill = header_fill
# Summary data
summary_data = [
("Total Payin", f"{total_payin:.2f}"),
("Total Payout", f"{total_payout:.2f}"),
("Net Profit", f"{total_payin - total_payout:.2f}"),
("Total Bets", str(total_bets)),
("Total Matches", str(len(extraction_stats)))
]
for i, (metric, value) in enumerate(summary_data, 4):
ws_summary[f'A{i}'] = metric
ws_summary[f'B{i}'] = value
ws_summary[f'A{i}'].border = border
ws_summary[f'B{i}'].border = border
# Auto-adjust column widths for summary
for col in ['A', 'B']:
ws_summary.column_dimensions[col].width = 20
# Generate Match Reports sheet
ws_matches['A1'] = f"Match Reports - {target_date.strftime('%Y-%m-%d')}"
ws_matches['A1'].font = Font(bold=True, size=16)
ws_matches.merge_cells('A1:H1')
# Headers
headers = ["Match #", "Fighter 1", "Fighter 2", "Venue", "Bets Count", "Payin", "Payout", "Net"]
for col, header in enumerate(headers, 1):
cell = ws_matches.cell(row=3, column=col, value=header)
cell.font = header_font
cell.fill = header_fill
cell.border = border
cell.alignment = Alignment(horizontal='center')
# Get match data
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
)
match_stats = {}
bet_details = bet_details_query.all()
for detail in bet_details:
match_id = detail.match_id
if match_id not in match_stats:
match_stats[match_id] = {
'match_id': match_id,
'bets_count': 0,
'payin': 0.0,
'payout': 0.0
}
match_stats[match_id]['bets_count'] += 1
match_stats[match_id]['payin'] += float(detail.amount)
# Calculate potential payout if bet was won
if detail.result in ['won', 'win']:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
match_stats[match_id]['payout'] += float(detail.amount) * float(odds)
# Write match data
row = 4
for match_id, stats in match_stats.items():
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
ws_matches.cell(row=row, column=1, value=match.match_number).border = border
ws_matches.cell(row=row, column=2, value=match.fighter1_township).border = border
ws_matches.cell(row=row, column=3, value=match.fighter2_township).border = border
ws_matches.cell(row=row, column=4, value=match.venue_kampala_township).border = border
ws_matches.cell(row=row, column=5, value=stats['bets_count']).border = border
ws_matches.cell(row=row, column=6, value=f"{stats['payin']:.2f}").border = border
ws_matches.cell(row=row, column=7, value=f"{stats['payout']:.2f}").border = border
ws_matches.cell(row=row, column=8, value=f"{stats['payin'] - stats['payout']:.2f}").border = border
row += 1
# Auto-adjust column widths for matches
for col in range(1, 9):
ws_matches.column_dimensions[get_column_letter(col)].width = 15
# Save workbook to memory buffer
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
# Return file for download
filename = f"report_{target_date.strftime('%Y-%m-%d')}.xlsx"
return send_file(
buffer,
as_attachment=True,
download_name=filename,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
)
finally:
session.close()
except Exception as e:
logger.error(f"API download Excel report error: {e}")
return jsonify({"error": str(e)}), 500
# Statistics API endpoints
@api_bp.route('/statistics')
@get_api_auth_decorator()
......
......@@ -64,10 +64,29 @@ window.Dashboard = (function() {
}
});
// Setup SocketIO event listeners if available
setupSocketIOListeners();
// Toast notifications
setupToastNotifications();
}
// Setup SocketIO event listeners
function setupSocketIOListeners() {
// Check if SocketIO is available
if (typeof io !== 'undefined' && window.socket) {
console.log('Setting up SocketIO event listeners for download progress');
// Listen for download progress events
window.socket.on('download_progress', function(data) {
console.log('Received download_progress via SocketIO:', data);
updateDownloadProgress(data);
});
} else {
console.log('SocketIO not available, using long polling for download progress');
}
}
// Setup offline/online detection
function setupOfflineDetection() {
window.addEventListener('online', function() {
......@@ -803,6 +822,10 @@ window.Dashboard = (function() {
}
}
}
else if (notification.type === 'download_progress') {
console.log('Download progress update:', notification.data);
updateDownloadProgress(notification.data);
}
else {
console.log('Unknown notification type:', notification.type);
}
......@@ -861,6 +884,56 @@ window.Dashboard = (function() {
return new Date(timestamp).toLocaleString();
}
// Download progress handling
function updateDownloadProgress(data) {
const progressContainer = document.getElementById('download-progress-container');
const progressBar = document.getElementById('download-progress-bar');
const progressText = document.getElementById('download-progress-text');
if (!progressContainer || !progressBar || !progressText) {
console.warn('Download progress elements not found');
return;
}
const percentage = data.percentage || 0;
const message = data.message || '';
const downloaded = data.downloaded || 0;
const total = data.total || 0;
// Show progress container
progressContainer.classList.remove('d-none');
// Update progress bar
progressBar.style.width = percentage + '%';
progressBar.setAttribute('aria-valuenow', percentage);
// Update progress text
if (total > 0) {
progressText.textContent = `${percentage}% (${downloaded}/${total})`;
} else {
progressText.textContent = message || `${percentage}%`;
}
// Change progress bar color based on completion
if (percentage >= 100) {
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.classList.add('bg-success');
// Hide progress after 3 seconds when complete
setTimeout(function() {
progressContainer.classList.add('d-none');
// Reset progress bar for next download
progressBar.style.width = '0%';
progressBar.classList.remove('bg-success');
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
}, 3000);
} else {
progressBar.classList.remove('bg-success');
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
}
console.log('Updated download progress:', data);
}
// Public API
return {
init: init,
......
......@@ -77,12 +77,12 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% if request.endpoint not in ['main.cashier_dashboard', 'main.cashier_bets', 'main.cashier_new_bet', 'main.cashier_bet_details', 'main.cashier_verify_bet_page'] %}
{% if request.endpoint not in ['main.cashier_dashboard', 'main.cashier_bets', 'main.cashier_new_bet', 'main.cashier_bet_details', 'main.cashier_verify_bet_page', 'main.cashier_reports'] %}
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.index' %}active{% endif %}"
href="{{ url_for('main.index') }}">
<i class="fas fa-tachometer-alt me-1"></i>Dashboard
<i class="fas fa-tachometer-alt me-1"></i>Home
</a>
</li>
<li class="nav-item">
......@@ -109,18 +109,6 @@
<i class="fas fa-cogs me-1"></i>Extraction
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.video_test' %}active{% endif %}"
href="{{ url_for('main.video_test') }}">
<i class="fas fa-upload me-1"></i>Video Test
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'screen_cast.screen_cast_dashboard' %}active{% endif %}"
href="{{ url_for('screen_cast.screen_cast_dashboard') }}">
<i class="fas fa-cast me-1"></i>Screen Cast
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.api_tokens' %}active{% endif %}"
href="{{ url_for('main.api_tokens') }}">
......@@ -133,6 +121,12 @@
<i class="fas fa-chart-bar me-1"></i>Statistics
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.reports' %}active{% endif %}"
href="{{ url_for('main.reports') }}">
<i class="fas fa-file-alt me-1"></i>Reports
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
......@@ -148,6 +142,9 @@
<li><a class="dropdown-item" href="{{ url_for('main.logs') }}">
<i class="fas fa-file-alt me-1"></i>Logs
</a></li>
<li><a class="dropdown-item" href="{{ url_for('screen_cast.screen_cast_dashboard') }}">
<i class="fas fa-cast me-1"></i>Screen Cast
</a></li>
</ul>
</li>
{% endif %}
......@@ -173,6 +170,12 @@
<i class="fas fa-qrcode me-1"></i>Verify Bet
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.cashier_reports' %}active{% endif %}"
href="{{ url_for('main.cashier_reports') }}">
<i class="fas fa-file-alt me-1"></i>Reports
</a>
</li>
</ul>
{% endif %}
......@@ -232,6 +235,22 @@
<!-- System Status Bar -->
<div id="status-bar" class="fixed-bottom bg-light border-top p-2 d-none d-lg-block">
<div class="container-fluid">
<!-- Download Progress Bar (hidden by default) -->
<div id="download-progress-container" class="row mb-2 d-none">
<div class="col-12">
<div class="d-flex align-items-center">
<span class="text-muted me-2">
<i class="fas fa-download me-1"></i>Downloading Fixtures:
</span>
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div id="download-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<small id="download-progress-text" class="text-muted">0%</small>
</div>
</div>
</div>
<!-- Status Information -->
<div class="row align-items-center text-small">
<div class="col-auto">
<span class="text-muted">Status:</span>
......
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="fas fa-chart-bar me-2"></i>Reports
<small class="text-muted">Welcome, {{ current_user.username }}</small>
</h1>
</div>
</div>
<!-- Back Button -->
<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-chart-line me-1"></i>Daily Reports
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<button class="btn btn-outline-secondary" onclick="window.location.href='/'">
<i class="fas fa-arrow-left me-2"></i>Back to Dashboard
</button>
</div>
<div class="col-md-6 mb-3 text-end">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-calendar"></i>
</span>
<input type="date" class="form-control" id="report-date-picker"
value="{{ today_date }}" max="{{ today_date }}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Daily Summary -->
<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-tachometer-alt me-2"></i>Daily Summary
<span class="badge bg-info ms-2" id="summary-date">{{ today_date }}</span>
<button class="btn btn-sm btn-success ms-2" onclick="downloadReport()">
<i class="fas fa-download me-1"></i>Download Excel
</button>
</h5>
</div>
<div class="card-body">
<div id="daily-summary-container">
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading daily summary...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Match Reports Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-list-ul me-2"></i>Match Reports
<span class="badge bg-info ms-2" id="matches-count">0</span>
</h5>
</div>
<div class="card-body">
<div id="match-reports-container">
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading match reports...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Match Details Modal -->
<div class="modal fade" id="matchDetailsModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-info-circle me-2"></i>Match Details - <span id="modal-match-title"></span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="match-details-container">
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading match details...
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Close
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/currency.js') }}"></script>
<script>
// Update currency symbols when settings are loaded
document.addEventListener('currencySettingsLoaded', function(event) {
// Update all currency amounts
document.querySelectorAll('.currency-amount').forEach(element => {
const amount = parseFloat(element.dataset.amount || 0);
element.textContent = formatCurrency(amount);
});
});
document.addEventListener('DOMContentLoaded', function() {
// Load reports on page load
loadReports();
// Date picker change event
document.getElementById('report-date-picker').addEventListener('change', function() {
loadReports();
});
});
// Function to load and display reports
function loadReports() {
console.log('🔍 loadReports() called');
const dateInput = document.getElementById('report-date-picker');
const selectedDate = dateInput.value;
// Update summary date badge
document.getElementById('summary-date').textContent = selectedDate;
// Load daily summary
loadDailySummary(selectedDate);
// Load match reports
loadMatchReports(selectedDate);
}
// Function to load daily summary
function loadDailySummary(date) {
const container = document.getElementById('daily-summary-container');
console.log('📡 Making API request to /api/reports/daily-summary for date:', date);
fetch(`/api/reports/daily-summary?date=${date}`)
.then(response => {
console.log('📡 Daily summary response status:', response.status);
if (!response.ok) {
throw new Error('API request failed: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('📦 Daily summary response data:', data);
if (data.success) {
updateDailySummary(data.summary);
} else {
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading daily summary: ${data.error || 'Unknown error'}
</div>
`;
}
})
.catch(error => {
console.error('❌ Error loading daily summary:', error);
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading daily summary: ${error.message}
</div>
`;
});
}
// Function to update daily summary display
function updateDailySummary(summary) {
const container = document.getElementById('daily-summary-container');
const summaryHTML = `
<div class="row text-center">
<div class="col-md-3 mb-3">
<div class="card h-100 border-success">
<div class="card-body">
<h5 class="card-title text-success">
<i class="fas fa-arrow-up me-2"></i>Payin
</h5>
<h3 class="currency-amount text-success" data-amount="${summary.total_payin}">${formatCurrency(summary.total_payin)}</h3>
<small class="text-muted">Total amount received</small>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card h-100 border-danger">
<div class="card-body">
<h5 class="card-title text-danger">
<i class="fas fa-arrow-down me-2"></i>Payout
</h5>
<h3 class="currency-amount text-danger" data-amount="${summary.total_payout}">${formatCurrency(summary.total_payout)}</h3>
<small class="text-muted">Total amount paid out</small>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card h-100 ${summary.net_profit >= 0 ? 'border-success' : 'border-danger'}">
<div class="card-body">
<h5 class="card-title ${summary.net_profit >= 0 ? 'text-success' : 'text-danger'}">
<i class="fas fa-${summary.net_profit >= 0 ? 'plus' : 'minus'} me-2"></i>Net Profit
</h5>
<h3 class="currency-amount ${summary.net_profit >= 0 ? 'text-success' : 'text-danger'}" data-amount="${summary.net_profit}">${formatCurrency(summary.net_profit)}</h3>
<small class="text-muted">Payin minus payout</small>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card h-100 border-info">
<div class="card-body">
<h5 class="card-title text-info">
<i class="fas fa-hashtag me-2"></i>Total Bets
</h5>
<h3 class="text-info">${summary.total_bets}</h3>
<small class="text-muted">Number of bets placed</small>
</div>
</div>
</div>
</div>
`;
container.innerHTML = summaryHTML;
}
// Function to load match reports
function loadMatchReports(date) {
const container = document.getElementById('match-reports-container');
const countBadge = document.getElementById('matches-count');
console.log('📡 Making API request to /api/reports/match-reports for date:', date);
fetch(`/api/reports/match-reports?date=${date}`)
.then(response => {
console.log('📡 Match reports response status:', response.status);
if (!response.ok) {
throw new Error('API request failed: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('📦 Match reports response data:', data);
if (data.success) {
// Update count badge
countBadge.textContent = data.total;
countBadge.className = data.total > 0 ? 'badge bg-info ms-2' : 'badge bg-secondary ms-2';
updateMatchReportsTable(data, container);
} else {
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading match reports: ${data.error || 'Unknown error'}
</div>
`;
}
})
.catch(error => {
console.error('❌ Error loading match reports:', error);
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading match reports: ${error.message}
</div>
`;
});
}
// Function to update match reports table
function updateMatchReportsTable(data, container) {
if (data.total === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>No matches found for the selected date
</div>
`;
return;
}
let tableHTML = `
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th><i class="fas fa-hashtag me-1"></i>Match #</th>
<th><i class="fas fa-users me-1"></i>Fighters</th>
<th><i class="fas fa-map-marker-alt me-1"></i>Venue</th>
<th><i class="fas fa-hashtag me-1"></i>Bets Count</th>
<th><i class="fas fa-arrow-up me-1"></i>Payin</th>
<th><i class="fas fa-arrow-down me-1"></i>Payout</th>
<th><i class="fas fa-chart-line me-1"></i>Net</th>
<th><i class="fas fa-cogs me-1"></i>Actions</th>
</tr>
</thead>
<tbody>
`;
data.matches.forEach(match => {
const payin = parseFloat(match.payin || 0);
const payout = parseFloat(match.payout || 0);
const net = parseFloat(match.net || 0);
tableHTML += `
<tr>
<td><strong>${match.match_number}</strong></td>
<td>
<div class="small">
<strong>${match.fighter1_township}</strong><br>
<span class="text-muted">vs</span><br>
<strong>${match.fighter2_township}</strong>
</div>
</td>
<td><small>${match.venue_kampala_township}</small></td>
<td><span class="badge bg-primary">${match.bets_count}</span></td>
<td><strong class="currency-amount text-success" data-amount="${payin}">${formatCurrency(payin)}</strong></td>
<td><strong class="currency-amount text-danger" data-amount="${payout}">${formatCurrency(payout)}</strong></td>
<td><strong class="currency-amount ${net >= 0 ? 'text-success' : 'text-danger'}" data-amount="${net}">${formatCurrency(net)}</strong></td>
<td>
<button class="btn btn-sm btn-outline-primary"
onclick="showMatchDetails('${match.match_id}', '${data.date}')"
title="View Detailed Bets">
<i class="fas fa-eye"></i> Details
</button>
</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
}
// Function to show match details modal
function showMatchDetails(matchId, date) {
const modal = new bootstrap.Modal(document.getElementById('matchDetailsModal'));
const container = document.getElementById('match-details-container');
const titleElement = document.getElementById('modal-match-title');
// Set loading state
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading match details...
</div>
`;
// Load match details
fetch(`/api/reports/match-details?match_id=${matchId}&date=${date}`)
.then(response => {
if (!response.ok) {
throw new Error('API request failed: ' + response.status);
}
return response.json();
})
.then(data => {
if (data.success) {
updateMatchDetailsModal(data, container, titleElement);
} else {
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading match details: ${data.error || 'Unknown error'}
</div>
`;
}
})
.catch(error => {
console.error('❌ Error loading match details:', error);
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading match details: ${error.message}
</div>
`;
});
modal.show();
}
// Function to update match details modal
function updateMatchDetailsModal(data, container, titleElement) {
// Update title
if (data.match) {
titleElement.textContent = `Match #${data.match.match_number}: ${data.match.fighter1_township} vs ${data.match.fighter2_township}`;
} else {
titleElement.textContent = `Match Details`;
}
if (data.total === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>No bets found for this match on the selected date
</div>
`;
return;
}
let tableHTML = `
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead class="table-dark">
<tr>
<th><i class="fas fa-clock me-1"></i>Time</th>
<th><i class="fas fa-ticket-alt me-1"></i>Bet ID</th>
<th><i class="fas fa-target me-1"></i>Outcome</th>
<th><i class="fas fa-dollar-sign me-1"></i>Amount</th>
<th><i class="fas fa-chart-line me-1"></i>Result</th>
<th><i class="fas fa-trophy me-1"></i>Potential Win</th>
</tr>
</thead>
<tbody>
`;
data.bets.forEach(bet => {
const amount = parseFloat(bet.amount || 0);
const potentialWin = parseFloat(bet.potential_winning || 0);
const betTime = new Date(bet.bet_datetime).toLocaleString();
let resultBadge = '';
if (bet.result === 'won') {
resultBadge = '<span class="badge bg-success"><i class="fas fa-trophy me-1"></i>Won</span>';
} else if (bet.result === 'lost') {
resultBadge = '<span class="badge bg-danger"><i class="fas fa-times me-1"></i>Lost</span>';
} else {
resultBadge = '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Pending</span>';
}
tableHTML += `
<tr>
<td><small>${betTime}</small></td>
<td><small>${bet.bet_id.substring(0, 8)}...</small></td>
<td><strong>${bet.outcome}</strong></td>
<td><strong class="currency-amount" data-amount="${amount}">${formatCurrency(amount)}</strong></td>
<td>${resultBadge}</td>
<td><strong class="currency-amount text-success" data-amount="${potentialWin}">${formatCurrency(potentialWin)}</strong></td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
}
// Function to download Excel report
function downloadReport() {
const dateInput = document.getElementById('report-date-picker');
const selectedDate = dateInput.value;
if (!selectedDate) {
alert('Please select a date for the report');
return;
}
// Create download link
const downloadUrl = `/api/reports/download-excel?date=${selectedDate}`;
const link = document.createElement('a');
link.href = downloadUrl;
link.download = `report_${selectedDate}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -37,6 +37,9 @@ opencv-python>=4.5.0
Pillow>=9.0.0
python-barcode[images]>=0.14.0
# Excel file generation
openpyxl>=3.0.0
# Screen capture and streaming (optional dependencies)
ffmpeg-python>=0.2.0
pychromecast>=13.0.0
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Progress Bar</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<style>
body { padding: 20px; }
.fixed-bottom { position: fixed; bottom: 0; left: 0; right: 0; }
</style>
</head>
<body>
<h1>Test Progress Bar</h1>
<button id="test-progress" class="btn btn-primary">Test Progress Bar</button>
<!-- System Status Bar -->
<div id="status-bar" class="fixed-bottom bg-light border-top p-2 d-none d-lg-block">
<div class="container-fluid">
<!-- Download Progress Bar (hidden by default) -->
<div id="download-progress-container" class="row mb-2 d-none">
<div class="col-12">
<div class="d-flex align-items-center">
<span class="text-muted me-2">
<i class="fas fa-download me-1"></i>Downloading Fixtures:
</span>
<div class="progress flex-grow-1 me-2" style="height: 8px;">
<div id="download-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<small id="download-progress-text" class="text-muted">0%</small>
</div>
</div>
</div>
<!-- Status Information -->
<div class="row align-items-center text-small">
<div class="col-auto">
<span class="text-muted">Status:</span>
<span id="system-status" class="badge bg-success">Online</span>
</div>
<div class="col-auto">
<span class="text-muted">Video:</span>
<span id="video-status" class="badge bg-secondary">Stopped</span>
</div>
<div class="col-auto">
<span class="text-muted">Match Timer:</span>
<span id="match-timer" class="badge bg-warning text-dark">--:--</span>
</div>
<div class="col-auto">
<span class="text-muted">Last Updated:</span>
<span id="last-updated" class="text-muted">--</span>
</div>
<div class="col text-end">
<small class="text-muted">Test v1.0.0</small>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Test function to simulate download progress
function updateDownloadProgress(data) {
const progressContainer = document.getElementById('download-progress-container');
const progressBar = document.getElementById('download-progress-bar');
const progressText = document.getElementById('download-progress-text');
if (!progressContainer || !progressBar || !progressText) {
console.warn('Download progress elements not found');
return;
}
const percentage = data.percentage || 0;
const message = data.message || '';
const downloaded = data.downloaded || 0;
const total = data.total || 0;
// Show progress container
progressContainer.classList.remove('d-none');
// Update progress bar
progressBar.style.width = percentage + '%';
progressBar.setAttribute('aria-valuenow', percentage);
// Update progress text
if (total > 0) {
progressText.textContent = `${percentage}% (${downloaded}/${total})`;
} else {
progressText.textContent = message || `${percentage}%`;
}
// Change progress bar color based on completion
if (percentage >= 100) {
progressBar.classList.remove('progress-bar-striped', 'progress-bar-animated');
progressBar.classList.add('bg-success');
// Hide progress after 3 seconds when complete
setTimeout(function() {
progressContainer.classList.add('d-none');
// Reset progress bar for next download
progressBar.style.width = '0%';
progressBar.classList.remove('bg-success');
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
}, 3000);
} else {
progressBar.classList.remove('bg-success');
progressBar.classList.add('progress-bar-striped', 'progress-bar-animated');
}
console.log('Updated download progress:', data);
}
// Test button click handler
document.getElementById('test-progress').addEventListener('click', function() {
// Simulate a download progress sequence
let progress = 0;
const total = 5;
let downloaded = 0;
const interval = setInterval(function() {
if (downloaded < total) {
downloaded++;
progress = Math.round((downloaded / total) * 100);
updateDownloadProgress({
downloaded: downloaded,
total: total,
percentage: progress,
message: `Downloading file ${downloaded}/${total}...`
});
} else {
// Final completion
updateDownloadProgress({
downloaded: total,
total: total,
percentage: 100,
message: `Downloads completed - ${total}/${total} files`
});
clearInterval(interval);
}
}, 1000);
});
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment