Enhance cashier dashboard with full fixture view and date filtering

- Changed from 'pending matches' to 'all matches in today's fixture'
- Added summary row showing counts by status (total, pending, bet, ingame, done, other)
- Added Result and Under/Over columns to the match table
- Made 'Bet' status badge clickable - links to new bet page for that match
- Added date filter to select past fixtures by date
- Updated API endpoint to support date parameter for filtering
- Color-coded rows based on match status (done=green, failed=red, ingame=blue, bet=yellow)
parent 84fe418f
......@@ -4122,7 +4122,7 @@ def calculate_fixture_status(matches, today, db_manager=None):
@api_bp.route('/cashier/pending-matches')
@get_api_auth_decorator()
def get_cashier_pending_matches():
"""Get pending matches from the correct fixture for cashier dashboard"""
"""Get matches from the fixture for cashier dashboard - supports date filtering"""
try:
# Allow access from localhost without authentication
if request.remote_addr == '127.0.0.1':
......@@ -4151,6 +4151,9 @@ def get_cashier_pending_matches():
session = api_bp.db_manager.get_session()
try:
# Check for date parameter in query string
date_param = request.args.get('date')
# Get today's date
today = date.today()
yesterday = today - timedelta(days=1)
......@@ -4158,20 +4161,33 @@ def get_cashier_pending_matches():
# First, auto-fail old fixtures with pending/scheduled/bet results
auto_fail_old_fixtures(session, yesterday)
# Find fixtures with start_time of today (get the LAST one, not first)
fixtures_with_today_start = session.query(MatchModel.fixture_id)\
# Determine the target date for fixture selection
if date_param:
# Parse the date parameter (format: YYYY-MM-DD)
try:
target_date = datetime.strptime(date_param, '%Y-%m-%d').date()
except ValueError:
return jsonify({
"success": False,
"error": f"Invalid date format: {date_param}. Use YYYY-MM-DD format."
}), 400
else:
target_date = today
# Find fixtures with start_time on the target date (get the LAST one)
fixtures_with_target_start = session.query(MatchModel.fixture_id)\
.filter(MatchModel.start_time.isnot(None))\
.filter(MatchModel.start_time >= datetime.combine(today, datetime.min.time()))\
.filter(MatchModel.start_time < datetime.combine(today, datetime.max.time()))\
.filter(MatchModel.start_time >= datetime.combine(target_date, datetime.min.time()))\
.filter(MatchModel.start_time < datetime.combine(target_date, datetime.max.time()))\
.order_by(MatchModel.created_at.desc())\
.first()
selected_fixture_id = None
if fixtures_with_today_start:
# Use the LAST fixture where matches start today
selected_fixture_id = fixtures_with_today_start.fixture_id
logger.info(f"Selected fixture {selected_fixture_id} - last fixture with matches starting today")
if fixtures_with_target_start:
# Use the LAST fixture where matches start on target date
selected_fixture_id = fixtures_with_target_start.fixture_id
logger.info(f"Selected fixture {selected_fixture_id} - last fixture with matches starting on {target_date}")
else:
# Fallback: find the first fixture where ALL matches are in pending status (not scheduled/bet)
all_fixtures = session.query(MatchModel.fixture_id).distinct().order_by(MatchModel.fixture_id.asc()).all()
......@@ -4192,7 +4208,8 @@ def get_cashier_pending_matches():
"success": True,
"matches": [],
"total": 0,
"fixture_id": None
"fixture_id": None,
"date": target_date.isoformat() if target_date else None
})
# Get all matches from the selected fixture
......@@ -4210,7 +4227,8 @@ def get_cashier_pending_matches():
"success": True,
"matches": matches_data,
"total": len(matches_data),
"fixture_id": selected_fixture_id
"fixture_id": selected_fixture_id,
"date": target_date.isoformat() if target_date else None
})
finally:
......
......@@ -98,19 +98,64 @@
<!-- Main Content Layout -->
<div class="row">
<!-- Pending Matches from First Fixture - Left Side -->
<!-- Matches from Fixture - Left Side -->
<div class="col-lg-9 col-md-8">
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Pending Matches - First Fixture
<span class="badge bg-warning ms-2" id="pending-matches-count">0</span>
</h5>
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Today's Fixture Matches
<span class="badge bg-primary ms-2" id="total-matches-count">0</span>
</h5>
<div class="d-flex gap-2 align-items-center">
<label class="form-label mb-0 me-1 small">Date:</label>
<input type="date" class="form-control form-control-sm" id="fixture-date-filter" style="width: 150px;">
</div>
</div>
</div>
<div class="card-body">
<div id="pending-matches-container">
<!-- Summary Row -->
<div class="row mb-3">
<div class="col-md-2 col-4 text-center">
<div class="badge bg-secondary p-2 w-100">
<div id="summary-total">0</div>
<small>Total</small>
</div>
</div>
<div class="col-md-2 col-4 text-center">
<div class="badge bg-warning p-2 w-100">
<div id="summary-pending">0</div>
<small>Pending</small>
</div>
</div>
<div class="col-md-2 col-4 text-center">
<div class="badge bg-success p-2 w-100">
<div id="summary-bet">0</div>
<small>Bet</small>
</div>
</div>
<div class="col-md-2 col-4 text-center">
<div class="badge bg-info p-2 w-100">
<div id="summary-ingame">0</div>
<small>In Game</small>
</div>
</div>
<div class="col-md-2 col-4 text-center">
<div class="badge bg-dark p-2 w-100">
<div id="summary-done">0</div>
<small>Done</small>
</div>
</div>
<div class="col-md-2 col-4 text-center">
<div class="badge bg-danger p-2 w-100">
<div id="summary-other">0</div>
<small>Other</small>
</div>
</div>
</div>
<div id="matches-container">
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading pending matches...
<i class="fas fa-spinner fa-spin me-2"></i>Loading matches...
</div>
</div>
</div>
......@@ -329,9 +374,34 @@ document.addEventListener('DOMContentLoaded', function() {
// Load available templates on page load
loadAvailableTemplates();
// Load pending matches for cashier dashboard
// Load fixture matches for cashier dashboard
loadPendingMatches();
// Set up date filter with today's date as default
const dateFilter = document.getElementById('fixture-date-filter');
if (dateFilter) {
// Set default to today
const today = new Date().toISOString().split('T')[0];
dateFilter.value = today;
// Listen for date changes
dateFilter.addEventListener('change', function() {
const selectedDateValue = this.value;
const today = new Date().toISOString().split('T')[0];
if (selectedDateValue === today) {
selectedDate = null; // null means today (default behavior)
} else {
selectedDate = selectedDateValue;
}
// Force reload with new date
isInitialMatchLoad = true;
cachedMatchesData = null;
loadPendingMatches();
});
}
// Quick action buttons
document.getElementById('btn-start-games').addEventListener('click', function() {
// Show confirmation dialog for starting games
......@@ -524,16 +594,17 @@ document.addEventListener('DOMContentLoaded', function() {
let cachedMatchesData = null;
let isInitialMatchLoad = true;
let selectedDate = null; // null means today
// Function to load and display pending matches
// Function to load and display fixture matches
function loadPendingMatches() {
console.log('🔍 loadPendingMatches() called');
console.log('🔍 loadFixtureMatches() called');
const container = document.getElementById('pending-matches-container');
const countBadge = document.getElementById('pending-matches-count');
const container = document.getElementById('matches-container');
const countBadge = document.getElementById('total-matches-count');
if (!container) {
console.error('❌ pending-matches-container not found');
console.error('❌ matches-container not found');
return;
}
......@@ -543,12 +614,18 @@ function loadPendingMatches() {
if (isInitialMatchLoad) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-spinner fa-spin me-2"></i>Loading pending matches...
<i class="fas fa-spinner fa-spin me-2"></i>Loading matches...
</div>
`;
}
fetch('/api/cashier/pending-matches')
// Build URL with optional date filter
let url = '/api/cashier/pending-matches';
if (selectedDate) {
url += '?date=' + selectedDate;
}
fetch(url)
.then(response => {
console.log('📡 API response status:', response.status);
if (!response.ok) {
......@@ -570,7 +647,9 @@ function loadPendingMatches() {
// Update count badge
countBadge.textContent = data.total;
countBadge.className = data.total > 0 ? 'badge bg-warning ms-2' : 'badge bg-success ms-2';
// Update summary counts
updateSummaryCounts(data.matches);
updateMatchesTable(data, container);
} else {
......@@ -598,11 +677,44 @@ function loadPendingMatches() {
});
}
function updateSummaryCounts(matches) {
const counts = {
total: matches.length,
pending: 0,
bet: 0,
ingame: 0,
done: 0,
other: 0
};
matches.forEach(match => {
const status = match.status || 'pending';
if (status === 'pending' || status === 'scheduled') {
counts.pending++;
} else if (status === 'bet') {
counts.bet++;
} else if (status === 'ingame') {
counts.ingame++;
} else if (status === 'done') {
counts.done++;
} else {
counts.other++;
}
});
document.getElementById('summary-total').textContent = counts.total;
document.getElementById('summary-pending').textContent = counts.pending;
document.getElementById('summary-bet').textContent = counts.bet;
document.getElementById('summary-ingame').textContent = counts.ingame;
document.getElementById('summary-done').textContent = counts.done;
document.getElementById('summary-other').textContent = counts.other;
}
function updateMatchesTable(data, container) {
if (data.total === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-check-circle me-2"></i>No pending matches found
<i class="fas fa-check-circle me-2"></i>No matches found for this fixture
</div>
`;
return;
......@@ -612,18 +724,19 @@ function updateMatchesTable(data, container) {
let tbody = container.querySelector('tbody');
if (!tbody) {
// Create new table
// Create new table with additional columns
container.innerHTML = `
<div class="table-responsive">
<table class="table table-striped table-hover">
<table class="table table-striped table-hover table-sm">
<thead class="table-dark">
<tr>
<th><i class="fas fa-hashtag me-1"></i>Match #</th>
<th style="width: 50px;"><i class="fas fa-hashtag me-1"></i>#</th>
<th><i class="fas fa-user-friends me-1"></i>Fighter 1</th>
<th><i class="fas fa-user-friends me-1"></i>Fighter 2</th>
<th><i class="fas fa-map-marker-alt me-1"></i>Venue</th>
<th><i class="fas fa-clock me-1"></i>Start Time</th>
<th><i class="fas fa-info-circle me-1"></i>Status</th>
<th style="width: 80px;"><i class="fas fa-info-circle me-1"></i>Status</th>
<th style="width: 70px;"><i class="fas fa-trophy me-1"></i>Result</th>
<th style="width: 70px;"><i class="fas fa-chart-line me-1"></i>U/O</th>
</tr>
</thead>
<tbody>
......@@ -638,64 +751,91 @@ function updateMatchesTable(data, container) {
const existingRows = Array.from(tbody.children);
const existingMatches = new Map();
existingRows.forEach(row => {
const matchNumber = row.getAttribute('data-match-number');
if (matchNumber) {
existingMatches.set(matchNumber, row);
const matchId = row.getAttribute('data-match-id');
if (matchId) {
existingMatches.set(matchId, row);
}
});
const processedMatches = new Set();
data.matches.forEach(match => {
const matchNumber = match.match_number.toString();
processedMatches.add(matchNumber);
const matchId = match.id.toString();
processedMatches.add(matchId);
const startTime = match.start_time ?
new Date(match.start_time).toLocaleString() : 'Not scheduled';
new Date(match.start_time).toLocaleTimeString() : 'N/A';
const status = match.status || 'pending';
let statusBadge = '';
switch (status) {
case 'scheduled':
statusBadge = '<span class="badge bg-primary"><i class="fas fa-calendar-check me-1"></i>Scheduled</span>';
statusBadge = '<span class="badge bg-primary"><i class="fas fa-calendar-check me-1"></i>Sched</span>';
break;
case 'bet':
statusBadge = '<span class="badge bg-success"><i class="fas fa-dollar-sign me-1"></i>Bet</span>';
statusBadge = `<a href="${pageUrl('/cashier/new-bet?match_id=' + match.id)}" class="badge bg-success text-decoration-none" title="Click to place bet"><i class="fas fa-dollar-sign me-1"></i>Bet</a>`;
break;
case 'ingame':
statusBadge = '<span class="badge bg-info"><i class="fas fa-play me-1"></i>In Game</span>';
statusBadge = '<span class="badge bg-info"><i class="fas fa-play me-1"></i>Live</span>';
break;
case 'completed':
statusBadge = '<span class="badge bg-success"><i class="fas fa-check me-1"></i>Completed</span>';
case 'done':
statusBadge = '<span class="badge bg-dark"><i class="fas fa-check me-1"></i>Done</span>';
break;
case 'cancelled':
statusBadge = '<span class="badge bg-secondary"><i class="fas fa-times me-1"></i>Cancelled</span>';
statusBadge = '<span class="badge bg-secondary"><i class="fas fa-times me-1"></i>Canc</span>';
break;
case 'failed':
statusBadge = '<span class="badge bg-danger"><i class="fas fa-exclamation-triangle me-1"></i>Failed</span>';
statusBadge = '<span class="badge bg-danger"><i class="fas fa-exclamation-triangle me-1"></i>Fail</span>';
break;
case 'paused':
statusBadge = '<span class="badge bg-warning"><i class="fas fa-pause me-1"></i>Paused</span>';
statusBadge = '<span class="badge bg-warning"><i class="fas fa-pause me-1"></i>Pause</span>';
break;
default:
statusBadge = '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Pending</span>';
statusBadge = '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Pend</span>';
}
// Result display
let resultDisplay = '-';
if (match.result) {
resultDisplay = `<span class="badge bg-success">${match.result}</span>`;
}
// Under/Over display
let underOverDisplay = '-';
if (match.under_over_result) {
const uoClass = match.under_over_result === 'UNDER' ? 'bg-info' : 'bg-warning';
underOverDisplay = `<span class="badge ${uoClass}">${match.under_over_result}</span>`;
}
// Row class based on status
let rowClass = '';
if (status === 'done') {
rowClass = 'table-success';
} else if (status === 'failed' || status === 'cancelled') {
rowClass = 'table-danger';
} else if (status === 'ingame') {
rowClass = 'table-info';
} else if (status === 'bet') {
rowClass = 'table-warning';
}
const newRowHTML = `
<td><strong>${match.match_number}</strong></td>
<td>${match.fighter1_township}</td>
<td>${match.fighter2_township}</td>
<td><strong>#${match.match_number}</strong></td>
<td><strong>${match.fighter1_township}</strong></td>
<td><strong>${match.fighter2_township}</strong></td>
<td>${match.venue_kampala_township}</td>
<td>${startTime}</td>
<td>${statusBadge}</td>
<td>${resultDisplay}</td>
<td>${underOverDisplay}</td>
`;
const existingRow = existingMatches.get(matchNumber);
const existingRow = existingMatches.get(matchId);
if (existingRow) {
// Update existing row only if content changed
if (existingRow.innerHTML !== newRowHTML) {
existingRow.innerHTML = newRowHTML;
existingRow.className = rowClass;
existingRow.style.backgroundColor = '#fff3cd'; // Highlight briefly
setTimeout(() => {
existingRow.style.backgroundColor = '';
......@@ -704,7 +844,8 @@ function updateMatchesTable(data, container) {
} else {
// Add new row
const row = document.createElement('tr');
row.setAttribute('data-match-number', matchNumber);
row.setAttribute('data-match-id', matchId);
row.className = rowClass;
row.innerHTML = newRowHTML;
row.style.backgroundColor = '#d4edda'; // Highlight new row
tbody.appendChild(row);
......@@ -715,8 +856,8 @@ function updateMatchesTable(data, container) {
});
// Remove rows no longer in data
existingMatches.forEach((row, matchNumber) => {
if (!processedMatches.has(matchNumber)) {
existingMatches.forEach((row, matchId) => {
if (!processedMatches.has(matchId)) {
row.style.backgroundColor = '#f8d7da'; // Highlight removed row
setTimeout(() => {
if (row.parentNode) {
......
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