Enhance reports page with improved layout, pagination, and detailed navigation

- Repositioned summary cards to 2 rows (3 cards in first row, 2 in second row)
- Changed table headers from 'Number of Matches' to 'Matches' and 'Number of Bets' to 'Bets'
- Added professional styling with gradient headers, hover effects, and enhanced card design
- Implemented full-width layout for reports page
- Added per-page selector (10, 20, 50, 100 options, default 50) with pagination
- Fixed 'View Details' button to properly navigate to client report detail page
- Created match report detail page with match information and bets list
- Created bet detail page with bet information and bet details breakdown
- Added breadcrumb navigation throughout all detail pages
- All pages maintain consistent professional styling and preserve filters
parent 66045355
...@@ -1557,7 +1557,7 @@ def reports(): ...@@ -1557,7 +1557,7 @@ def reports():
# Pagination # Pagination
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100) per_page = min(request.args.get('per_page', 50, type=int), 100)
# Calculate date range based on filter # Calculate date range based on filter
now = datetime.utcnow() now = datetime.utcnow()
...@@ -1834,7 +1834,8 @@ def reports(): ...@@ -1834,7 +1834,8 @@ def reports():
'start_time': start_time_filter, 'start_time': start_time_filter,
'end_time': end_time_filter, 'end_time': end_time_filter,
'sort_by': sort_by, 'sort_by': sort_by,
'sort_order': sort_order 'sort_order': sort_order,
'per_page': per_page
}) })
except Exception as e: except Exception as e:
...@@ -1858,7 +1859,8 @@ def reports(): ...@@ -1858,7 +1859,8 @@ def reports():
'start_time': '', 'start_time': '',
'end_time': '', 'end_time': '',
'sort_by': 'sync_timestamp', 'sort_by': 'sync_timestamp',
'sort_order': 'desc' 'sort_order': 'desc',
'per_page': 50
}) })
@csrf.exempt @csrf.exempt
...@@ -2452,6 +2454,10 @@ def client_report_detail(client_id): ...@@ -2452,6 +2454,10 @@ def client_report_detail(client_id):
start_time_filter = request.args.get('start_time', '').strip() start_time_filter = request.args.get('start_time', '').strip()
end_time_filter = request.args.get('end_time', '').strip() end_time_filter = request.args.get('end_time', '').strip()
# Pagination
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 50, type=int), 100)
# Calculate date range # Calculate date range
now = datetime.utcnow() now = datetime.utcnow()
start_date = None start_date = None
...@@ -2524,8 +2530,11 @@ def client_report_detail(client_id): ...@@ -2524,8 +2530,11 @@ def client_report_detail(client_id):
if end_date: if end_date:
query = query.filter(MatchReport.match_datetime <= end_date) query = query.filter(MatchReport.match_datetime <= end_date)
# Get all matching match reports for this client # Get all matching match reports for this client with pagination
match_reports = query.order_by(MatchReport.match_datetime.desc()).all() match_reports_pagination = query.order_by(MatchReport.match_datetime.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
match_reports = match_reports_pagination.items
# Calculate totals from match reports # Calculate totals from match reports
total_payin = sum(float(r.total_payin) for r in match_reports if r.total_payin) total_payin = sum(float(r.total_payin) for r in match_reports if r.total_payin)
...@@ -2547,6 +2556,7 @@ def client_report_detail(client_id): ...@@ -2547,6 +2556,7 @@ def client_report_detail(client_id):
client_id=client_id, client_id=client_id,
token_name=token_name, token_name=token_name,
match_reports=match_reports, match_reports=match_reports,
pagination=match_reports_pagination,
totals={ totals={
'total_payin': total_payin, 'total_payin': total_payin,
'total_payout': total_payout, 'total_payout': total_payout,
...@@ -2564,7 +2574,8 @@ def client_report_detail(client_id): ...@@ -2564,7 +2574,8 @@ def client_report_detail(client_id):
'start_date': start_date_filter, 'start_date': start_date_filter,
'end_date': end_date_filter, 'end_date': end_date_filter,
'start_time': start_time_filter, 'start_time': start_time_filter,
'end_time': end_time_filter 'end_time': end_time_filter,
'per_page': per_page
}) })
except Exception as e: except Exception as e:
...@@ -2577,4 +2588,203 @@ def client_report_detail(client_id): ...@@ -2577,4 +2588,203 @@ def client_report_detail(client_id):
end_date=end_date_filter if 'end_date_filter' in locals() else '', end_date=end_date_filter if 'end_date_filter' in locals() else '',
start_time=start_time_filter if 'start_time_filter' in locals() else '', start_time=start_time_filter if 'start_time_filter' in locals() else '',
end_time=end_time_filter if 'end_time_filter' in locals() else '')) end_time=end_time_filter if 'end_time_filter' in locals() else ''))
return redirect(url_for('main.sync_logs')) @csrf.exempt
\ No newline at end of file @bp.route('/client-report/<client_id>/match/<int:match_id>')
@login_required
@require_active_user
def match_report_detail(client_id, match_id):
"""Detailed view of a specific match with bets"""
try:
from app.models import MatchReport, Bet, BetDetail, APIToken, ClientActivity
from datetime import timedelta
# Get filter parameters
date_range_filter = request.args.get('date_range', 'today').strip()
start_date_filter = request.args.get('start_date', '').strip()
end_date_filter = request.args.get('end_date', '').strip()
start_time_filter = request.args.get('start_time', '').strip()
end_time_filter = request.args.get('end_time', '').strip()
# Pagination
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 50, type=int), 100)
# Calculate date range
now = datetime.utcnow()
start_date = None
end_date = None
if date_range_filter == 'today':
start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now
elif date_range_filter == 'yesterday':
yesterday = now - timedelta(days=1)
start_date = yesterday.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = yesterday.replace(hour=23, minute=59, second=59, microsecond=999999)
elif date_range_filter == 'this_week':
start_date = now - timedelta(days=now.weekday())
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now
elif date_range_filter == 'last_week':
last_week_end = now - timedelta(days=now.weekday() + 1)
last_week_start = last_week_end - timedelta(days=6)
start_date = last_week_start.replace(hour=0, minute=0, second=0, microsecond=0)
end_date = last_week_end.replace(hour=23, minute=59, second=59, microsecond=999999)
elif date_range_filter == 'this_month':
start_date = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end_date = now
elif date_range_filter == 'all':
start_date = None
end_date = None
elif date_range_filter == 'custom':
if start_date_filter:
try:
start_date = datetime.strptime(start_date_filter, '%Y-%m-%d')
if start_time_filter:
hour, minute = map(int, start_time_filter.split(':'))
start_date = start_date.replace(hour=hour, minute=minute, second=0, microsecond=0)
else:
start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0)
except ValueError:
pass
if end_date_filter:
try:
end_date = datetime.strptime(end_date_filter, '%Y-%m-%d')
if end_time_filter:
hour, minute = map(int, end_time_filter.split(':'))
end_date = end_date.replace(hour=hour, minute=minute, second=59, microsecond=999999)
else:
end_date = end_date.replace(hour=23, minute=59, second=59, microsecond=999999)
except ValueError:
pass
# Check if user has access to this client
if current_user.is_admin:
query = MatchReport.query.filter_by(client_id=client_id, match_id=match_id)
else:
user_token_ids = [t.id for t in APIToken.query.filter_by(user_id=current_user.id).all()]
if user_token_ids:
client_ids = [c.rustdesk_id for c in ClientActivity.query.filter(
ClientActivity.api_token_id.in_(user_token_ids)
).all()]
if client_id not in client_ids:
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
query = MatchReport.query.filter_by(client_id=client_id, match_id=match_id)
else:
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
# Apply date filters
if start_date:
query = query.filter(MatchReport.match_datetime >= start_date)
if end_date:
query = query.filter(MatchReport.match_datetime <= end_date)
# Get the match report
match_report = query.first()
if not match_report:
flash('Match report not found', 'error')
return redirect(url_for('main.client_report_detail', client_id=client_id))
# Get bets for this match with pagination
bets_query = Bet.query.filter_by(client_id=client_id, match_id=match_id)
# Apply date filters to bets
if start_date:
bets_query = bets_query.filter(Bet.bet_datetime >= start_date)
if end_date:
bets_query = bets_query.filter(Bet.bet_datetime <= end_date)
bets_pagination = bets_query.order_by(Bet.bet_datetime.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
# Get client token name
client_activity = ClientActivity.query.filter_by(rustdesk_id=client_id).first()
token_name = client_activity.api_token.name if client_activity and client_activity.api_token else 'Unknown'
return render_template('main/match_report_detail.html',
client_id=client_id,
token_name=token_name,
match_id=match_id,
match_report=match_report,
bets=bets_pagination.items,
pagination=bets_pagination,
filters={
'date_range': date_range_filter,
'start_date': start_date_filter,
'end_date': end_date_filter,
'start_time': start_time_filter,
'end_time': end_time_filter,
'per_page': per_page
})
except Exception as e:
logger.error(f"Match report detail error: {str(e)}")
flash('Error loading match report details', 'error')
@csrf.exempt
@bp.route('/client-report/<client_id>/match/<int:match_id>/bet/<bet_uuid>')
@login_required
@require_active_user
def bet_detail(client_id, match_id, bet_uuid):
"""Detailed view of a specific bet with bet details"""
try:
from app.models import Bet, BetDetail, APIToken, ClientActivity
# Get filter parameters
date_range_filter = request.args.get('date_range', 'today').strip()
start_date_filter = request.args.get('start_date', '').strip()
end_date_filter = request.args.get('end_date', '').strip()
start_time_filter = request.args.get('start_time', '').strip()
end_time_filter = request.args.get('end_time', '').strip()
# Check if user has access to this client
if current_user.is_admin:
pass # Admin can access all
else:
user_token_ids = [t.id for t in APIToken.query.filter_by(user_id=current_user.id).all()]
if user_token_ids:
client_ids = [c.rustdesk_id for c in ClientActivity.query.filter(
ClientActivity.api_token_id.in_(user_token_ids)
).all()]
if client_id not in client_ids:
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
else:
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
# Get the bet
bet = Bet.query.filter_by(client_id=client_id, match_id=match_id, uuid=bet_uuid).first()
if not bet:
flash('Bet not found', 'error')
return redirect(url_for('main.match_report_detail', client_id=client_id, match_id=match_id))
# Get bet details
bet_details = BetDetail.query.filter_by(bet_id=bet.id).all()
# Get client token name
client_activity = ClientActivity.query.filter_by(rustdesk_id=client_id).first()
token_name = client_activity.api_token.name if client_activity and client_activity.api_token else 'Unknown'
return render_template('main/bet_detail.html',
client_id=client_id,
token_name=token_name,
match_id=match_id,
bet=bet,
bet_details=bet_details,
filters={
'date_range': date_range_filter,
'start_date': start_date_filter,
'end_date': end_date_filter,
'start_time': start_time_filter,
'end_time': end_time_filter
})
except Exception as e:
logger.error(f"Bet detail error: {str(e)}")
flash('Error loading bet details', 'error')
return redirect(url_for('main.match_report_detail', client_id=client_id, match_id=match_id))
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Bet Detail - {{ bet.uuid }}{% endblock %}
{% block extra_css %}
<style>
/* Override container max-width for full-width layout */
.container {
max-width: 100% !important;
padding: 0 1rem !important;
}
/* Enhanced table styling */
.table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
.table thead th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
border: none;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
}
.table thead th:first-child {
border-top-left-radius: 8px;
}
.table thead th:last-child {
border-top-right-radius: 8px;
}
.table tbody tr {
transition: all 0.2s ease;
border-bottom: 1px solid #e9ecef;
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: scale(1.005);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 16px;
vertical-align: middle;
}
.table tbody tr:last-child td:first-child {
border-bottom-left-radius: 8px;
}
.table tbody tr:last-child td:last-child {
border-bottom-right-radius: 8px;
}
/* Enhanced card styling */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-bottom: 1px solid #e9ecef;
border-radius: 12px 12px 0 0 !important;
padding: 16px 20px;
}
/* Enhanced button styling */
.btn-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-info:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
color: white;
}
/* Summary card styling */
.summary-card {
border-radius: 8px;
padding: 1rem;
transition: all 0.3s ease;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>Bet Details</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.reports', **filters) }}">Reports</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('main.client_report_detail', client_id=client_id, **filters) }}">{{ token_name }} ({{ client_id }})</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=match_id, **filters) }}">Match {{ match_id }}</a></li>
<li class="breadcrumb-item active">Bet {{ bet.uuid }}</li>
</ol>
</nav>
</div>
<a href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=match_id, **filters) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Match
</a>
</div>
<!-- Bet Summary Cards -->
<div class="row mb-3">
<div class="col-md-4">
<div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Bet UUID</h6>
<h4 class="mb-0 fw-bold"><code>{{ bet.uuid }}</code></h4>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Total Amount</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(bet.total_amount) if bet.total_amount else '0.00' }}</h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-gradient-success text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Bet Count</h6>
<h3 class="mb-0 fw-bold">{{ bet.bet_count }}</h3>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Match Number</h6>
<h3 class="mb-0 fw-bold">{{ bet.match_number }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Paid</h6>
<h3 class="mb-0 fw-bold">
{% if bet.paid %}
<i class="fas fa-check-circle"></i> Yes
{% else %}
<i class="fas fa-times-circle"></i> No
{% endif %}
</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-success text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Paid Out</h6>
<h3 class="mb-0 fw-bold">
{% if bet.paid_out %}
<i class="fas fa-check-circle"></i> Yes
{% else %}
<i class="fas fa-times-circle"></i> No
{% endif %}
</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Bet DateTime</h6>
<h5 class="mb-0 fw-bold">{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M:%S') if bet.bet_datetime else '' }}</h5>
</div>
</div>
</div>
</div>
<!-- Bet Details Table -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-list me-2"></i>Bet Details</h5>
</div>
<div class="card-body">
{% if bet_details %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Match ID</th>
<th>Match Number</th>
<th>Outcome</th>
<th>Amount</th>
<th>Win Amount</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{% for detail in bet_details %}
<tr>
<td>{{ detail.match_id }}</td>
<td>{{ detail.match_number }}</td>
<td><strong>{{ detail.outcome }}</strong></td>
<td class="text-end">{{ "{:,.2f}".format(detail.amount) if detail.amount else '0.00' }}</td>
<td class="text-end">{{ "{:,.2f}".format(detail.win_amount) if detail.win_amount else '0.00' }}</td>
<td>
{% if detail.result == 'win' %}
<span class="badge bg-success">Win</span>
{% elif detail.result == 'lose' %}
<span class="badge bg-danger">Lose</span>
{% elif detail.result == 'pending' %}
<span class="badge bg-secondary">Pending</span>
{% else %}
<span class="badge bg-info">{{ detail.result }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5">
<i class="fas fa-list fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No bet details found</h4>
<p class="text-muted">This bet has no detailed breakdown.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
...@@ -2,6 +2,176 @@ ...@@ -2,6 +2,176 @@
{% block title %}Client Report - {{ token_name }}{% endblock %} {% block title %}Client Report - {{ token_name }}{% endblock %}
{% block extra_css %}
<style>
/* Override container max-width for full-width layout */
.container {
max-width: 100% !important;
padding: 0 1rem !important;
}
/* Enhanced table styling */
.table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
.table thead th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
border: none;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
}
.table thead th:first-child {
border-top-left-radius: 8px;
}
.table thead th:last-child {
border-top-right-radius: 8px;
}
.table tbody tr {
transition: all 0.2s ease;
border-bottom: 1px solid #e9ecef;
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: scale(1.005);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 16px;
vertical-align: middle;
}
.table tbody tr:last-child td:first-child {
border-bottom-left-radius: 8px;
}
.table tbody tr:last-child td:last-child {
border-bottom-right-radius: 8px;
}
/* Enhanced card styling */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-bottom: 1px solid #e9ecef;
border-radius: 12px 12px 0 0 !important;
padding: 16px 20px;
}
/* Enhanced button styling */
.btn-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-info:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
color: white;
}
/* Enhanced pagination styling */
.pagination .page-link {
color: #667eea;
border: 1px solid #dee2e6;
margin: 0 4px;
border-radius: 6px;
padding: 8px 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.pagination .page-link:hover {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.pagination .page-item.active .page-link {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
color: white;
}
/* Per page selector styling */
.per-page-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: #495057;
}
.per-page-selector select {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
font-size: 0.9rem;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.per-page-selector select:hover {
border-color: #667eea;
}
.per-page-selector select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Table info text */
.table-info {
color: #6c757d;
font-size: 0.9rem;
margin-top: 12px;
}
/* Summary card styling */
.summary-card {
border-radius: 8px;
padding: 1rem;
transition: all 0.3s ease;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
...@@ -11,97 +181,103 @@ ...@@ -11,97 +181,103 @@
<h1>Client Report Details</h1> <h1>Client Report Details</h1>
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.reports') }}">Reports</a></li> <li class="breadcrumb-item"><a href="{{ url_for('main.reports', **filters) }}">Reports</a></li>
<li class="breadcrumb-item active">{{ token_name }} ({{ client_id }})</li> <li class="breadcrumb-item active">{{ token_name }} ({{ client_id }})</li>
</ol> </ol>
</nav> </nav>
</div> </div>
<a href="{{ url_for('main.reports') }}" class="btn btn-secondary"> <a href="{{ url_for('main.reports', **filters) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Reports <i class="fas fa-arrow-left"></i> Back to Reports
</a> </a>
</div> </div>
<!-- Summary Card --> <!-- Summary Cards - First Row (3 cards) -->
<div class="card mb-4"> <div class="row mb-3">
<div class="card-header"> <div class="col-md-4">
<h5 class="mb-0"><i class="fas fa-chart-line"></i> Summary</h5> <div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="icon-box bg-white bg-opacity-25 rounded-circle p-2">
<i class="fas fa-arrow-down fa-lg"></i>
</div>
</div>
<div class="flex-grow-1 ms-2">
<h6 class="card-title mb-1 text-white-50">Total Payin</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.total_payin) }}</h3>
</div> </div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="d-flex align-items-center">
<div class="col-md-3"> <div class="flex-shrink-0">
<div class="p-3 bg-light rounded"> <div class="icon-box bg-white bg-opacity-25 rounded-circle p-2">
<h6 class="text-muted mb-1">Client ID</h6> <i class="fas fa-arrow-up fa-lg"></i>
<p class="mb-0"><code>{{ client_id }}</code></p>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="flex-grow-1 ms-2">
<div class="p-3 bg-light rounded"> <h6 class="card-title mb-1 text-white-50">Total Payout</h6>
<h6 class="text-muted mb-1">Token Name</h6> <h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.total_payout) }}</h3>
<p class="mb-0">{{ token_name }}</p>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="p-3 bg-light rounded">
<h6 class="text-muted mb-1">Total Matches</h6>
<p class="mb-0">{{ totals.total_matches }}</p>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="p-3 bg-light rounded">
<h6 class="text-muted mb-1">Total Bets</h6>
<p class="mb-0">{{ totals.total_bets }}</p>
</div> </div>
<div class="col-md-4">
<div class="card {% if totals.total_balance >= 0 %}bg-gradient-success{% else %}bg-gradient-danger{% endif %} text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="icon-box bg-white bg-opacity-25 rounded-circle p-2">
<i class="fas fa-balance-scale fa-lg"></i>
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="flex-grow-1 ms-2">
<div class="col-md-3"> <h6 class="card-title mb-1 text-white-50">Total Balance</h6>
<div class="p-3 bg-primary text-white rounded"> <h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.total_balance) }}</h3>
<h6 class="mb-1">Total Payin</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(totals.total_payin) }}</h3>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="p-3 bg-info text-white rounded">
<h6 class="mb-1">Total Payout</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(totals.total_payout) }}</h3>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="p-3 {% if totals.total_balance >= 0 %}bg-success{% else %}bg-danger{% endif %} text-white rounded">
<h6 class="mb-1">Balance</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(totals.total_balance) }}</h3>
</div> </div>
</div> </div>
<div class="col-md-3">
<div class="p-3 bg-warning text-white rounded"> <!-- Summary Cards - Second Row (2 cards) -->
<h6 class="mb-1">CAP Compensation Balance</h6> <div class="row mb-4">
<h3 class="mb-0">{{ "{:,.2f}".format(totals.cap_balance) }}</h3> <div class="col-md-6">
<div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="icon-box bg-white bg-opacity-25 rounded-circle p-2">
<i class="fas fa-exchange-alt fa-lg"></i>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="flex-grow-1 ms-2">
<div class="p-3 bg-info text-white rounded"> <h6 class="card-title mb-1 text-white-50">CAP Redistribution</h6>
<h6 class="mb-1">Accumulated Shortfall</h6> <h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.cap_balance) }}</h3>
<h3 class="mb-0">{{ "{:,.2f}".format(totals.accumulated_shortfall) }}</h3>
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-3">
<div class="col-md-4">
<div class="p-3 bg-success text-white rounded">
<h6 class="mb-1">Winning Bets</h6>
<h3 class="mb-0">{{ totals.winning_bets }}</h3>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-6">
<div class="p-3 bg-danger text-white rounded"> <div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<h6 class="mb-1">Losing Bets</h6> <div class="card-body">
<h3 class="mb-0">{{ totals.losing_bets }}</h3> <div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="icon-box bg-white bg-opacity-25 rounded-circle p-2">
<i class="fas fa-chart-line fa-lg"></i>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="flex-grow-1 ms-2">
<div class="p-3 bg-secondary text-white rounded"> <h6 class="card-title mb-1 text-white-50">Accumulated Shortfall</h6>
<h6 class="mb-1">Pending Bets</h6> <h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.accumulated_shortfall) }}</h3>
<h3 class="mb-0">{{ totals.pending_bets }}</h3> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -109,14 +285,24 @@ ...@@ -109,14 +285,24 @@
</div> </div>
<!-- Filters --> <!-- Filters -->
<div class="card mb-4"> <div class="card mb-4 shadow-sm">
<div class="card-header"> <div class="card-header bg-white">
<h5 class="mb-0"><i class="fas fa-filter"></i> Filters</h5> <button class="btn btn-link text-decoration-none w-100 text-start p-0" type="button" data-bs-toggle="collapse" data-bs-target="#filterCollapse" aria-expanded="true" aria-controls="filterCollapse">
<h5 class="mb-0 d-flex align-items-center">
<i class="fas fa-filter me-2 text-primary"></i> Filters
<i class="fas fa-chevron-down ms-auto" id="filterIcon"></i>
</h5>
</button>
</div> </div>
<div class="collapse show" id="filterCollapse">
<div class="card-body"> <div class="card-body">
<form method="GET" action="{{ url_for('main.client_report_detail', client_id=client_id) }}" class="row g-3"> <form method="GET" action="{{ url_for('main.client_report_detail', client_id=client_id) }}">
<div class="col-md-3"> <div class="row">
<label for="date_range" class="form-label">Date Range</label> <!-- Date Range Selection -->
<div class="col-md-4 mb-3">
<label for="date_range" class="form-label fw-semibold">
<i class="fas fa-calendar me-1"></i> Date Range
</label>
<select class="form-select" id="date_range" name="date_range"> <select class="form-select" id="date_range" name="date_range">
<option value="today" {% if filters.date_range == 'today' %}selected{% endif %}>Today</option> <option value="today" {% if filters.date_range == 'today' %}selected{% endif %}>Today</option>
<option value="yesterday" {% if filters.date_range == 'yesterday' %}selected{% endif %}>Yesterday</option> <option value="yesterday" {% if filters.date_range == 'yesterday' %}selected{% endif %}>Yesterday</option>
...@@ -127,40 +313,73 @@ ...@@ -127,40 +313,73 @@
<option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>Custom</option> <option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>Custom</option>
</select> </select>
</div> </div>
<div class="col-md-2"> </div>
<label for="start_date" class="form-label">Start Date</label>
<!-- Time Filters (Always Visible) -->
<div class="row mb-3">
<div class="col-md-3">
<label for="start_date" class="form-label fw-semibold">
<i class="fas fa-play me-1"></i> Start Date
</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ filters.start_date }}"> <input type="date" class="form-control" id="start_date" name="start_date" value="{{ filters.start_date }}">
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<label for="start_time" class="form-label">Start Time</label> <label for="start_time" class="form-label fw-semibold">
<i class="fas fa-clock me-1"></i> Start Time
</label>
<input type="time" class="form-control" id="start_time" name="start_time" value="{{ filters.start_time }}"> <input type="time" class="form-control" id="start_time" name="start_time" value="{{ filters.start_time }}">
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<label for="end_date" class="form-label">End Date</label> <label for="end_date" class="form-label fw-semibold">
<i class="fas fa-stop me-1"></i> End Date
</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ filters.end_date }}"> <input type="date" class="form-control" id="end_date" name="end_date" value="{{ filters.end_date }}">
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<label for="end_time" class="form-label">End Time</label> <label for="end_time" class="form-label fw-semibold">
<i class="fas fa-clock me-1"></i> End Time
</label>
<input type="time" class="form-control" id="end_time" name="end_time" value="{{ filters.end_time }}"> <input type="time" class="form-control" id="end_time" name="end_time" value="{{ filters.end_time }}">
</div> </div>
<div class="col-12"> </div>
<!-- Action Buttons -->
<div class="row">
<div class="col-12 text-end">
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetFilters()">
<i class="fas fa-undo me-1"></i> Reset Filters
</button>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Apply Filters <i class="fas fa-search me-1"></i> Apply Filters
</button> </button>
<a href="{{ url_for('main.client_report_detail', client_id=client_id) }}" class="btn btn-secondary"> </div>
<i class="fas fa-times"></i> Clear Filters
</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div>
<!-- Match Reports Table --> <!-- Match Reports Table -->
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="fas fa-list me-2 text-primary"></i>Match Reports
</h5>
<div class="per-page-selector">
<span>Show:</span>
<select id="perPage" onchange="changePerPage()">
<option value="10" {% if filters.per_page == 10 %}selected{% endif %}>10</option>
<option value="20" {% if filters.per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if filters.per_page == 50 or not filters.per_page %}selected{% endif %}>50</option>
<option value="100" {% if filters.per_page == 100 %}selected{% endif %}>100</option>
</select>
<span>per page</span>
</div>
</div>
<div class="card-body"> <div class="card-body">
{% if match_reports %} {% if match_reports %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Match ID</th> <th>Match ID</th>
...@@ -179,6 +398,7 @@ ...@@ -179,6 +398,7 @@
<th>CAP Applied</th> <th>CAP Applied</th>
<th>CAP Balance</th> <th>CAP Balance</th>
<th>Accumulated Shortfall</th> <th>Accumulated Shortfall</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -208,11 +428,59 @@ ...@@ -208,11 +428,59 @@
</td> </td>
<td class="text-end">{{ "{:,.2f}".format(report.cap_compensation_balance) if report.cap_compensation_balance else '0.00' }}</td> <td class="text-end">{{ "{:,.2f}".format(report.cap_compensation_balance) if report.cap_compensation_balance else '0.00' }}</td>
<td class="text-end">{{ "{:,.2f}".format(report.accumulated_shortfall) if report.accumulated_shortfall else '0.00' }}</td> <td class="text-end">{{ "{:,.2f}".format(report.accumulated_shortfall) if report.accumulated_shortfall else '0.00' }}</td>
<td>
<a href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=report.match_id, **filters) }}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> View Details
</a>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<!-- Pagination -->
{% if pagination and pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.client_report_detail', client_id=client_id, page=pagination.prev_num, **filters) }}">Previous</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num == pagination.page %}
<li class="page-item active"><span class="page-link">{{ page_num }}</span></li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.client_report_detail', client_id=client_id, page=page_num, **filters) }}">{{ page_num }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.client_report_detail', client_id=client_id, page=pagination.next_num, **filters) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
<div class="d-flex justify-content-between align-items-center mt-3">
<p class="table-info mb-0">
Showing {{ pagination.per_page * (pagination.page - 1) + 1 }} to
{{ min(pagination.per_page * pagination.page, pagination.total) }} of
{{ pagination.total }} matches
</p>
<div class="per-page-selector">
<span>Page {{ pagination.page }} of {{ pagination.pages }}</span>
</div>
</div>
{% endif %}
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i> <i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
...@@ -225,4 +493,38 @@ ...@@ -225,4 +493,38 @@
</div> </div>
</div> </div>
</div> </div>
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const filterCollapse = document.getElementById('filterCollapse');
const filterIcon = document.getElementById('filterIcon');
const dateRangeSelect = document.getElementById('date_range');
// Handle collapsible filter animation
if (filterCollapse && filterIcon) {
filterCollapse.addEventListener('show.bs.collapse', function() {
filterIcon.classList.remove('fa-chevron-up');
filterIcon.classList.add('fa-chevron-down');
});
filterCollapse.addEventListener('hide.bs.collapse', function() {
filterIcon.classList.remove('fa-chevron-down');
filterIcon.classList.add('fa-chevron-up');
});
}
});
function resetFilters() {
window.location.href = "{{ url_for('main.client_report_detail', client_id=client_id) }}";
}
function changePerPage() {
const perPage = document.getElementById('perPage').value;
const url = new URL(window.location.href);
url.searchParams.set('per_page', perPage);
url.searchParams.set('page', '1');
window.location.href = url.toString();
}
</script>
{% endblock %}
{% endblock %} {% endblock %}
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Match Report - Match {{ match_id }}{% endblock %}
{% block extra_css %}
<style>
/* Override container max-width for full-width layout */
.container {
max-width: 100% !important;
padding: 0 1rem !important;
}
/* Enhanced table styling */
.table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
.table thead th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
border: none;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
}
.table thead th:first-child {
border-top-left-radius: 8px;
}
.table thead th:last-child {
border-top-right-radius: 8px;
}
.table tbody tr {
transition: all 0.2s ease;
border-bottom: 1px solid #e9ecef;
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: scale(1.005);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 16px;
vertical-align: middle;
}
.table tbody tr:last-child td:first-child {
border-bottom-left-radius: 8px;
}
.table tbody tr:last-child td:last-child {
border-bottom-right-radius: 8px;
}
/* Enhanced card styling */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-bottom: 1px solid #e9ecef;
border-radius: 12px 12px 0 0 !important;
padding: 16px 20px;
}
/* Enhanced button styling */
.btn-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-info:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
color: white;
}
/* Enhanced pagination styling */
.pagination .page-link {
color: #667eea;
border: 1px solid #dee2e6;
margin: 0 4px;
border-radius: 6px;
padding: 8px 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.pagination .page-link:hover {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.pagination .page-item.active .page-link {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
color: white;
}
/* Per page selector styling */
.per-page-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: #495057;
}
.per-page-selector select {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
font-size: 0.9rem;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.per-page-selector select:hover {
border-color: #667eea;
}
.per-page-selector select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Table info text */
.table-info {
color: #6c757d;
font-size: 0.9rem;
margin-top: 12px;
}
/* Summary card styling */
.summary-card {
border-radius: 8px;
padding: 1rem;
transition: all 0.3s ease;
}
.summary-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>Match Report Details</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.reports', **filters) }}">Reports</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('main.client_report_detail', client_id=client_id, **filters) }}">{{ token_name }} ({{ client_id }})</a></li>
<li class="breadcrumb-item active">Match {{ match_id }}</li>
</ol>
</nav>
</div>
<a href="{{ url_for('main.client_report_detail', client_id=client_id, **filters) }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Client
</a>
</div>
<!-- Match Summary Cards -->
<div class="row mb-3">
<div class="col-md-3">
<div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Match ID</h6>
<h3 class="mb-0 fw-bold">{{ match_report.match_id }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Match Number</h6>
<h3 class="mb-0 fw-bold">{{ match_report.match_number }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-success text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Total Bets</h6>
<h3 class="mb-0 fw-bold">{{ match_report.total_bets }}</h3>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Balance</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(match_report.balance) if match_report.balance else '0.00' }}</h3>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-4">
<div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Total Payin</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(match_report.total_payin) if match_report.total_payin else '0.00' }}</h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">Total Payout</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(match_report.total_payout) if match_report.total_payout else '0.00' }}</h3>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<h6 class="card-title mb-1 text-white-50">CAP Balance</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(match_report.cap_compensation_balance) if match_report.cap_compensation_balance else '0.00' }}</h3>
</div>
</div>
</div>
</div>
<!-- Match Details Card -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0"><i class="fas fa-info-circle me-2"></i>Match Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<strong>Fixture ID:</strong> <code>{{ match_report.fixture_id }}</code>
</div>
<div class="col-md-6 mb-3">
<strong>Match DateTime:</strong> {{ match_report.match_datetime.strftime('%Y-%m-%d %H:%M:%S') if match_report.match_datetime else '' }}
</div>
<div class="col-md-6 mb-3">
<strong>Actual Result:</strong> {{ match_report.actual_result }}
</div>
<div class="col-md-6 mb-3">
<strong>Extraction Result:</strong> {{ match_report.extraction_result }}
</div>
<div class="col-md-6 mb-3">
<strong>CAP Applied:</strong>
{% if match_report.cap_applied %}
<span class="text-success"><i class="fas fa-check-circle"></i> {{ match_report.cap_percentage }}%</span>
{% else %}
<span class="text-danger"><i class="fas fa-times-circle"></i> No</span>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<strong>Accumulated Shortfall:</strong> {{ "{:,.2f}".format(match_report.accumulated_shortfall) if match_report.accumulated_shortfall else '0.00' }}
</div>
</div>
</div>
</div>
<!-- Bets Table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="fas fa-list me-2 text-primary"></i>Bets for this Match
</h5>
<div class="per-page-selector">
<span>Show:</span>
<select id="perPage" onchange="changePerPage()">
<option value="10" {% if filters.per_page == 10 %}selected{% endif %}>10</option>
<option value="20" {% if filters.per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if filters.per_page == 50 or not filters.per_page %}selected{% endif %}>50</option>
<option value="100" {% if filters.per_page == 100 %}selected{% endif %}>100</option>
</select>
<span>per page</span>
</div>
</div>
<div class="card-body">
{% if bets %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Bet UUID</th>
<th>Bet DateTime</th>
<th>Match Number</th>
<th>Total Amount</th>
<th>Bet Count</th>
<th>Paid</th>
<th>Paid Out</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for bet in bets %}
<tr>
<td><code>{{ bet.uuid }}</code></td>
<td>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M:%S') if bet.bet_datetime else '' }}</td>
<td>{{ bet.match_number }}</td>
<td class="text-end">{{ "{:,.2f}".format(bet.total_amount) if bet.total_amount else '0.00' }}</td>
<td class="text-center">{{ bet.bet_count }}</td>
<td class="text-center">
{% if bet.paid %}
<i class="fas fa-check-circle text-success"></i>
{% else %}
<i class="fas fa-times-circle text-danger"></i>
{% endif %}
</td>
<td class="text-center">
{% if bet.paid_out %}
<i class="fas fa-check-circle text-success"></i>
{% else %}
<i class="fas fa-times-circle text-danger"></i>
{% endif %}
</td>
<td>
<a href="{{ url_for('main.bet_detail', client_id=client_id, match_id=match_id, bet_uuid=bet.uuid, **filters) }}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> View Details
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination and pagination.pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=match_id, page=pagination.prev_num, **filters) }}">Previous</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num == pagination.page %}
<li class="page-item active"><span class="page-link">{{ page_num }}</span></li>
{% else %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=match_id, page=page_num, **filters) }}">{{ page_num }}</a>
</li>
{% endif %}
{% else %}
<li class="page-item disabled"><span class="page-link">...</span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('main.match_report_detail', client_id=client_id, match_id=match_id, page=pagination.next_num, **filters) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
<div class="d-flex justify-content-between align-items-center mt-3">
<p class="table-info mb-0">
Showing {{ pagination.per_page * (pagination.page - 1) + 1 }} to
{{ min(pagination.per_page * pagination.page, pagination.total) }} of
{{ pagination.total }} bets
</p>
<div class="per-page-selector">
<span>Page {{ pagination.page }} of {{ pagination.pages }}</span>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-5">
<i class="fas fa-coins fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No bets found for this match</h4>
<p class="text-muted">Try adjusting your filters or wait for clients to sync reports.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% block extra_js %}
<script>
function changePerPage() {
const perPage = document.getElementById('perPage').value;
const url = new URL(window.location.href);
url.searchParams.set('per_page', perPage);
url.searchParams.set('page', '1');
window.location.href = url.toString();
}
</script>
{% endblock %}
{% endblock %}
\ No newline at end of file
...@@ -2,6 +2,164 @@ ...@@ -2,6 +2,164 @@
{% block title %}Reports{% endblock %} {% block title %}Reports{% endblock %}
{% block extra_css %}
<style>
/* Override container max-width for full-width layout */
.container {
max-width: 100% !important;
padding: 0 1rem !important;
}
/* Enhanced table styling */
.table {
border-collapse: separate;
border-spacing: 0;
width: 100%;
}
.table thead th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 0.5px;
border: none;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
}
.table thead th:first-child {
border-top-left-radius: 8px;
}
.table thead th:last-child {
border-top-right-radius: 8px;
}
.table tbody tr {
transition: all 0.2s ease;
border-bottom: 1px solid #e9ecef;
}
.table tbody tr:hover {
background-color: #f8f9fa;
transform: scale(1.005);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.table tbody td {
padding: 14px 16px;
vertical-align: middle;
}
.table tbody tr:last-child td:first-child {
border-bottom-left-radius: 8px;
}
.table tbody tr:last-child td:last-child {
border-bottom-right-radius: 8px;
}
/* Enhanced card styling */
.card {
border: none;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
transition: all 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.card-header {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border-bottom: 1px solid #e9ecef;
border-radius: 12px 12px 0 0 !important;
padding: 16px 20px;
}
/* Enhanced button styling */
.btn-info {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
}
.btn-info:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
color: white;
}
/* Enhanced pagination styling */
.pagination .page-link {
color: #667eea;
border: 1px solid #dee2e6;
margin: 0 4px;
border-radius: 6px;
padding: 8px 14px;
font-weight: 500;
transition: all 0.2s ease;
}
.pagination .page-link:hover {
background-color: #667eea;
color: white;
border-color: #667eea;
}
.pagination .page-item.active .page-link {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: #667eea;
color: white;
}
/* Per page selector styling */
.per-page-selector {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
color: #495057;
}
.per-page-selector select {
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
font-size: 0.9rem;
background-color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.per-page-selector select:hover {
border-color: #667eea;
}
.per-page-selector select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Table info text */
.table-info {
color: #6c757d;
font-size: 0.9rem;
margin-top: 12px;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
...@@ -20,9 +178,9 @@ ...@@ -20,9 +178,9 @@
</div> </div>
</div> </div>
<!-- Summary Cards --> <!-- Summary Cards - First Row (3 cards) -->
<div class="row mb-4"> <div class="row mb-3">
<div class="col" style="flex: 0 0 20%; max-width: 20%;"> <div class="col-md-4">
<div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100"> <div class="card bg-gradient-primary text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
...@@ -39,7 +197,7 @@ ...@@ -39,7 +197,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col" style="flex: 0 0 20%; max-width: 20%;"> <div class="col-md-4">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100"> <div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
...@@ -56,7 +214,7 @@ ...@@ -56,7 +214,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col" style="flex: 0 0 20%; max-width: 20%;"> <div class="col-md-4">
<div class="card {% if totals.total_balance >= 0 %}bg-gradient-success{% else %}bg-gradient-danger{% endif %} text-white mb-3 shadow-sm border-0 h-100"> <div class="card {% if totals.total_balance >= 0 %}bg-gradient-success{% else %}bg-gradient-danger{% endif %} text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
...@@ -73,7 +231,11 @@ ...@@ -73,7 +231,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col" style="flex: 0 0 20%; max-width: 20%;"> </div>
<!-- Summary Cards - Second Row (2 cards) -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100"> <div class="card bg-gradient-warning text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
...@@ -90,7 +252,7 @@ ...@@ -90,7 +252,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col" style="flex: 0 0 20%; max-width: 20%;"> <div class="col-md-6">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100"> <div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body"> <div class="card-body">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
...@@ -222,20 +384,35 @@ ...@@ -222,20 +384,35 @@
<!-- Clients Table --> <!-- Clients Table -->
<div class="card"> <div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-semibold">
<i class="fas fa-users me-2 text-primary"></i>Client Reports
</h5>
<div class="per-page-selector">
<span>Show:</span>
<select id="perPage" onchange="changePerPage()">
<option value="10" {% if filters.per_page == 10 %}selected{% endif %}>10</option>
<option value="20" {% if filters.per_page == 20 %}selected{% endif %}>20</option>
<option value="50" {% if filters.per_page == 50 or not filters.per_page %}selected{% endif %}>50</option>
<option value="100" {% if filters.per_page == 100 %}selected{% endif %}>100</option>
</select>
<span>per page</span>
</div>
</div>
<div class="card-body"> <div class="card-body">
{% if clients %} {% if clients %}
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th>Client Name</th> <th>Client Name</th>
<th>Client ID</th> <th>Client ID</th>
<th>Number of Matches</th> <th>Matches</th>
<th>Number of Bets</th> <th>Bets</th>
<th>Payin</th> <th>Payin</th>
<th>Payout</th> <th>Payout</th>
<th>Balance</th> <th>Balance</th>
<th>CAP Redistribution Balance</th> <th>CAP Redistribution</th>
<th>Accumulated Shortfall</th> <th>Accumulated Shortfall</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
...@@ -255,7 +432,7 @@ ...@@ -255,7 +432,7 @@
<td class="text-end">{{ "{:,.2f}".format(client.cap_balance) }}</td> <td class="text-end">{{ "{:,.2f}".format(client.cap_balance) }}</td>
<td class="text-end">{{ "{:,.2f}".format(client.accumulated_shortfall) }}</td> <td class="text-end">{{ "{:,.2f}".format(client.accumulated_shortfall) }}</td>
<td> <td>
<a href="{{ url_for('main.client_report_detail', **filters) }}" class="btn btn-sm btn-info"> <a href="{{ url_for('main.client_report_detail', client_id=client.client_id, **filters) }}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> View Details <i class="fas fa-eye"></i> View Details
</a> </a>
</td> </td>
...@@ -296,11 +473,16 @@ ...@@ -296,11 +473,16 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>
<p class="text-center text-muted"> <div class="d-flex justify-content-between align-items-center mt-3">
<p class="table-info mb-0">
Showing {{ pagination.per_page * (pagination.page - 1) + 1 }} to Showing {{ pagination.per_page * (pagination.page - 1) + 1 }} to
{{ min(pagination.per_page * pagination.page, pagination.total) }} of {{ min(pagination.per_page * pagination.page, pagination.total) }} of
{{ pagination.total }} clients {{ pagination.total }} clients
</p> </p>
<div class="per-page-selector">
<span>Page {{ pagination.page }} of {{ pagination.pages }}</span>
</div>
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
...@@ -350,6 +532,14 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -350,6 +532,14 @@ document.addEventListener('DOMContentLoaded', function() {
function resetFilters() { function resetFilters() {
window.location.href = "{{ url_for('main.reports') }}"; window.location.href = "{{ url_for('main.reports') }}";
} }
function changePerPage() {
const perPage = document.getElementById('perPage').value;
const url = new URL(window.location.href);
url.searchParams.set('per_page', perPage);
url.searchParams.set('page', '1');
window.location.href = url.toString();
}
</script> </script>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}
\ 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