Update reports

parent e3eef4f3
This diff is collapsed.
This diff is collapsed.
......@@ -1041,6 +1041,9 @@ def api_reports_sync():
'details': f'Missing required field: {field}'
}), 400
# Get cap compensation balance (optional field)
cap_compensation_balance = data.get('cap_compensation_balance', 0.00)
# Check for duplicate sync_id (idempotency)
existing_sync = ReportSync.query.filter_by(sync_id=data['sync_id']).first()
if existing_sync:
......@@ -1111,12 +1114,16 @@ def api_reports_sync():
total_payout=summary['total_payout'],
net_profit=summary['net_profit'],
total_bets=summary['total_bets'],
total_matches=summary['total_matches']
total_matches=summary['total_matches'],
cap_compensation_balance=cap_compensation_balance
)
db.session.add(report_sync)
# Process bets
bets_count = 0
bets_new = 0
bets_duplicate = 0
for bet_data in data['bets']:
# Validate bet UUID
try:
......@@ -1131,7 +1138,26 @@ def api_reports_sync():
# Check for duplicate bet UUID
existing_bet = Bet.query.filter_by(uuid=bet_data['uuid']).first()
if existing_bet:
logger.warning(f"Duplicate bet UUID {bet_data['uuid']} detected, skipping")
# Update existing bet if it has changed
bet_updated = False
if existing_bet.total_amount != bet_data['total_amount']:
existing_bet.total_amount = bet_data['total_amount']
bet_updated = True
if existing_bet.paid != bet_data.get('paid', False):
existing_bet.paid = bet_data.get('paid', False)
bet_updated = True
if existing_bet.paid_out != bet_data.get('paid_out', False):
existing_bet.paid_out = bet_data.get('paid_out', False)
bet_updated = True
if bet_updated:
existing_bet.sync_id = report_sync.id
bets_count += 1
bets_duplicate += 1
logger.info(f"Updated existing bet {bet_data['uuid']}")
else:
logger.warning(f"Duplicate bet UUID {bet_data['uuid']} detected, skipping (no changes)")
bets_duplicate += 1
continue
# Parse bet datetime
......@@ -1160,6 +1186,7 @@ def api_reports_sync():
# Flush to get bet.id before creating bet details
db.session.flush()
bets_count += 1
bets_new += 1
# Process bet details
for detail_data in bet_data.get('details', []):
......@@ -1250,8 +1277,8 @@ def api_reports_sync():
operation_type='new_sync',
status='success',
bets_processed=bets_count,
bets_new=bets_count,
bets_duplicate=0,
bets_new=bets_new,
bets_duplicate=bets_duplicate,
stats_processed=stats_count,
stats_new=stats_new,
stats_updated=stats_updated,
......
......@@ -775,6 +775,61 @@ class Migration_010_CreateReportsTables(Migration):
def can_rollback(self) -> bool:
return True
class Migration_011_AddCapCompensationBalance(Migration):
"""Add cap_compensation_balance column to report_syncs table"""
def __init__(self):
super().__init__("011", "Add cap_compensation_balance column to report_syncs table")
def up(self):
"""Add cap_compensation_balance column"""
try:
# Check if column already exists
inspector = inspect(db.engine)
# Check if report_syncs table exists
if 'report_syncs' not in inspector.get_table_names():
logger.info("report_syncs table does not exist yet, skipping migration")
return True
columns = [col['name'] for col in inspector.get_columns('report_syncs')]
if 'cap_compensation_balance' in columns:
logger.info("cap_compensation_balance column already exists, skipping creation")
return True
# Add column
alter_table_sql = '''
ALTER TABLE report_syncs
ADD COLUMN cap_compensation_balance DECIMAL(15,2) NOT NULL DEFAULT 0.00
'''
with db.engine.connect() as conn:
conn.execute(text(alter_table_sql))
conn.commit()
logger.info("Added cap_compensation_balance column successfully")
return True
except Exception as e:
logger.error(f"Migration 011 failed: {str(e)}")
raise
def down(self):
"""Drop cap_compensation_balance column"""
try:
with db.engine.connect() as conn:
conn.execute(text("ALTER TABLE report_syncs DROP COLUMN IF EXISTS cap_compensation_balance"))
conn.commit()
logger.info("Dropped cap_compensation_balance column")
return True
except Exception as e:
logger.error(f"Rollback of migration 011 failed: {str(e)}")
raise
def can_rollback(self) -> bool:
return True
class MigrationManager:
"""Manages database migrations and versioning"""
......@@ -790,6 +845,7 @@ class MigrationManager:
Migration_008_AddRemoteDomainSetting(),
Migration_009_CreateClientActivityTable(),
Migration_010_CreateReportsTables(),
Migration_011_AddCapCompensationBalance(),
]
def ensure_version_table(self):
......
"""
Migration to add cap_compensation_balance field to report_syncs table
"""
from datetime import datetime
from app import db
from app.models import ReportSync
def upgrade():
"""Add cap_compensation_balance column to report_syncs table"""
try:
# Check if column already exists
inspector = db.inspect(db.engine)
columns = [col['name'] for col in inspector.get_columns('report_syncs')]
if 'cap_compensation_balance' not in columns:
# Add the column
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE report_syncs
ADD COLUMN cap_compensation_balance NUMERIC(15, 2) DEFAULT 0.00
"""))
db.session.commit()
print("Successfully added cap_compensation_balance column to report_syncs table")
else:
print("cap_compensation_balance column already exists in report_syncs table")
except Exception as e:
db.session.rollback()
print(f"Error adding cap_compensation_balance column: {str(e)}")
raise
def downgrade():
"""Remove cap_compensation_balance column from report_syncs table"""
try:
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE report_syncs
DROP COLUMN IF EXISTS cap_compensation_balance
"""))
db.session.commit()
print("Successfully removed cap_compensation_balance column from report_syncs table")
except Exception as e:
db.session.rollback()
print(f"Error removing cap_compensation_balance column: {str(e)}")
raise
if __name__ == '__main__':
print("Running migration: add_cap_compensation_balance")
upgrade()
\ No newline at end of file
......@@ -1541,14 +1541,16 @@ def clients():
def reports():
"""Reports page with filtering, pagination, and export"""
try:
from app.models import ReportSync, Bet, ExtractionStats
from app.models import ReportSync, Bet, ExtractionStats, APIToken, ClientActivity
from sqlalchemy import func, and_, or_
# Get filter parameters
client_id_filter = request.args.get('client_id', '').strip()
date_range_filter = request.args.get('date_range', '').strip()
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()
sort_by = request.args.get('sort_by', 'sync_timestamp')
sort_order = request.args.get('sort_order', 'desc')
export_format = request.args.get('export', '').strip()
......@@ -1557,16 +1559,66 @@ def reports():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 20, type=int), 100)
# Calculate date range based on filter
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 of current week (Monday)
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':
# Start of last week (Monday)
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':
# Use custom date range
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
# Base query
if current_user.is_admin:
query = ReportSync.query
else:
# Non-admin users can only see reports from their own clients
from app.models import APIToken
user_token_ids = [t.id for t in APIToken.query.filter_by(user_id=current_user.id).all()]
if user_token_ids:
# Get client_ids from ClientActivity for this user's tokens
from app.models import ClientActivity
client_ids = [c.rustdesk_id for c in ClientActivity.query.filter(
ClientActivity.api_token_id.in_(user_token_ids)
).all()]
......@@ -1581,21 +1633,11 @@ def reports():
if client_id_filter:
query = query.filter(ReportSync.client_id == client_id_filter)
if start_date_filter:
try:
start_date = datetime.strptime(start_date_filter, '%Y-%m-%d')
if start_date:
query = query.filter(ReportSync.start_date >= start_date)
except ValueError:
pass
if end_date_filter:
try:
end_date = datetime.strptime(end_date_filter, '%Y-%m-%d')
# Include the entire end date
end_date = end_date.replace(hour=23, minute=59, second=59)
if end_date:
query = query.filter(ReportSync.end_date <= end_date)
except ValueError:
pass
# Sorting
if hasattr(ReportSync, sort_by):
......@@ -1614,29 +1656,48 @@ def reports():
# Pagination
reports_pagination = query.paginate(page=page, per_page=per_page, error_out=False)
# Get unique client IDs for filter dropdown
# Get unique client IDs for filter dropdown with token names
if current_user.is_admin:
all_client_ids = db.session.query(ReportSync.client_id).distinct().all()
# Get all clients with their token names
clients_query = db.session.query(
ReportSync.client_id,
APIToken.name.label('token_name')
).join(
ClientActivity, ReportSync.client_id == ClientActivity.rustdesk_id
).join(
APIToken, ClientActivity.api_token_id == APIToken.id
).filter(
APIToken.is_active == True
).distinct().all()
client_data = [{'client_id': c.client_id, 'token_name': c.token_name} for c in clients_query]
else:
if user_token_ids:
from app.models import ClientActivity
all_client_ids = db.session.query(ClientActivity.rustdesk_id).filter(
ClientActivity.api_token_id.in_(user_token_ids)
clients_query = db.session.query(
ClientActivity.rustdesk_id.label('client_id'),
APIToken.name.label('token_name')
).join(
APIToken, ClientActivity.api_token_id == APIToken.id
).filter(
ClientActivity.api_token_id.in_(user_token_ids),
APIToken.is_active == True
).distinct().all()
else:
all_client_ids = []
client_ids = [cid[0] for cid in all_client_ids if cid[0]]
client_data = [{'client_id': c.client_id, 'token_name': c.token_name} for c in clients_query]
else:
client_data = []
return render_template('main/reports.html',
reports=reports_pagination.items,
pagination=reports_pagination,
client_ids=client_ids,
client_data=client_data,
filters={
'client_id': client_id_filter,
'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,
'sort_by': sort_by,
'sort_order': sort_order
})
......
......@@ -857,6 +857,9 @@ class ReportSync(db.Model):
total_bets = db.Column(db.Integer, default=0)
total_matches = db.Column(db.Integer, default=0)
# Cap compensation balance at the time of sync
cap_compensation_balance = db.Column(db.Numeric(15, 2), default=0.00)
# Metadata
created_at = db.Column(db.DateTime, default=datetime.utcnow)
......@@ -879,6 +882,7 @@ class ReportSync(db.Model):
'net_profit': float(self.net_profit) if self.net_profit else 0.0,
'total_bets': self.total_bets,
'total_matches': self.total_matches,
'cap_compensation_balance': float(self.cap_compensation_balance) if self.cap_compensation_balance else 0.0,
'created_at': self.created_at.isoformat() if self.created_at else None
}
......
......@@ -80,22 +80,28 @@
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
<div class="col-md-3">
<div class="p-3 bg-primary text-white rounded">
<h6 class="mb-1">Total Payin</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.total_payin) if report.total_payin else '0.00' }}</h3>
</div>
</div>
<div class="col-md-4">
<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(report.total_payout) if report.total_payout else '0.00' }}</h3>
</div>
</div>
<div class="col-md-4">
<div class="p-3 {% if report.net_profit >= 0 %}bg-success{% else %}bg-danger{% endif %} text-white rounded">
<h6 class="mb-1">Net Profit</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.net_profit) if report.net_profit else '0.00' }}</h3>
<div class="col-md-3">
<div class="p-3 {% if (report.total_payin - report.total_payout) >= 0 %}bg-success{% else %}bg-danger{% endif %} text-white rounded">
<h6 class="mb-1">Balance</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.total_payin - report.total_payout) if report.total_payin and report.total_payout else '0.00' }}</h3>
</div>
</div>
<div class="col-md-3">
<div class="p-3 bg-warning text-white rounded">
<h6 class="mb-1">CAP Redistribution Balance</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.cap_compensation_balance) if report.cap_compensation_balance else '0.00' }}</h3>
</div>
</div>
</div>
......@@ -189,7 +195,7 @@
<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>
<span class="badge bg-{% if detail.result == 'won' %}success{% elif detail.result == 'lost' %}danger{% elif detail.result == 'pending' %}warning{% else %}secondary{% endif %}">
<span class="badge bg-{% if detail.result == 'win' %}success{% elif detail.result == 'lost' %}danger{% elif detail.result == 'pending' %}warning{% else %}secondary{% endif %}">
{{ detail.result }}
</span>
</td>
......@@ -295,7 +301,6 @@
<th>Outcome</th>
<th>Bets</th>
<th>Amount</th>
<th>Coefficient</th>
</tr>
</thead>
<tbody>
......@@ -304,7 +309,6 @@
<td>{{ outcome }}</td>
<td>{{ data.bets }}</td>
<td>{{ "{:,.2f}".format(data.amount) if data.amount else '0.00' }}</td>
<td>{{ data.coefficient }}</td>
</tr>
{% endfor %}
</tbody>
......
......@@ -28,33 +28,43 @@
<div class="card-body">
<form method="GET" action="{{ url_for('main.reports') }}" class="row g-3">
<div class="col-md-3">
<label for="client_id" class="form-label">Client ID</label>
<label for="client_id" class="form-label">Client</label>
<select class="form-select" id="client_id" name="client_id">
<option value="">All Clients</option>
{% for cid in client_ids %}
<option value="{{ cid }}" {% if filters.client_id == cid %}selected{% endif %}>{{ cid }}</option>
{% for client in client_data %}
<option value="{{ client.client_id }}" {% if filters.client_id == client.client_id %}selected{% endif %}>{{ client.token_name }} ({{ client.client_id }})</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="date_range" class="form-label">Date Range</label>
<select class="form-select" id="date_range" name="date_range">
<option value="">All</option>
<select class="form-select" id="date_range" name="date_range" onchange="toggleCustomDateRange()">
<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="week" {% if filters.date_range == 'week' %}selected{% endif %}>This Week</option>
<option value="this_week" {% if filters.date_range == 'this_week' %}selected{% endif %}>This Week</option>
<option value="last_week" {% if filters.date_range == 'last_week' %}selected{% endif %}>Last Week</option>
<option value="this_month" {% if filters.date_range == 'this_month' %}selected{% endif %}>This Month</option>
<option value="all" {% if filters.date_range == 'all' %}selected{% endif %}>All Time</option>
<option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>Custom</option>
</select>
</div>
<div class="col-md-2">
<div class="col-md-2" id="custom-date-fields" style="display: none;">
<label for="start_date" class="form-label">Start Date</label>
<input type="date" class="form-control" id="start_date" name="start_date" value="{{ filters.start_date }}">
</div>
<div class="col-md-2">
<div class="col-md-1" id="custom-time-fields" style="display: none;">
<label for="start_time" class="form-label">Start Time</label>
<input type="time" class="form-control" id="start_time" name="start_time" value="{{ filters.start_time }}">
</div>
<div class="col-md-2" id="custom-date-fields-end" style="display: none;">
<label for="end_date" class="form-label">End Date</label>
<input type="date" class="form-control" id="end_date" name="end_date" value="{{ filters.end_date }}">
</div>
<div class="col-md-2">
<div class="col-md-1" id="custom-time-fields-end" style="display: none;">
<label for="end_time" class="form-label">End Time</label>
<input type="time" class="form-control" id="end_time" name="end_time" value="{{ filters.end_time }}">
</div>
<div class="col-md-1">
<label for="sort_by" class="form-label">Sort By</label>
<select class="form-select" id="sort_by" name="sort_by">
<option value="sync_timestamp" {% if filters.sort_by == 'sync_timestamp' %}selected{% endif %}>Sync Timestamp</option>
......@@ -93,12 +103,13 @@
<thead>
<tr>
<th>Sync ID</th>
<th>Client ID</th>
<th>Client</th>
<th>Sync Timestamp</th>
<th>Date Range</th>
<th>Total Payin</th>
<th>Total Payout</th>
<th>Net Profit</th>
<th>Balance</th>
<th>CAP Balance</th>
<th>Total Bets</th>
<th>Total Matches</th>
<th>Actions</th>
......@@ -113,8 +124,11 @@
<td>{{ report.date_range }}</td>
<td class="text-end">{{ "{:,.2f}".format(report.total_payin) if report.total_payin else '0.00' }}</td>
<td class="text-end">{{ "{:,.2f}".format(report.total_payout) if report.total_payout else '0.00' }}</td>
<td class="text-end {% if report.net_profit >= 0 %}text-success{% else %}text-danger{% endif %}">
{{ "{:,.2f}".format(report.net_profit) if report.net_profit else '0.00' }}
<td class="text-end {% if (report.total_payin - report.total_payout) >= 0 %}text-success{% else %}text-danger{% endif %}">
{{ "{:,.2f}".format(report.total_payin - report.total_payout) if report.total_payin and report.total_payout else '0.00' }}
</td>
<td class="text-end text-info">
{{ "{:,.2f}".format(report.cap_compensation_balance) if report.cap_compensation_balance else '0.00' }}
</td>
<td class="text-center">{{ report.total_bets }}</td>
<td class="text-center">{{ report.total_matches }}</td>
......@@ -178,4 +192,31 @@
</div>
</div>
</div>
<script>
function toggleCustomDateRange() {
var dateRange = document.getElementById('date_range').value;
var customDateFields = document.getElementById('custom-date-fields');
var customTimeFields = document.getElementById('custom-time-fields');
var customDateFieldsEnd = document.getElementById('custom-date-fields-end');
var customTimeFieldsEnd = document.getElementById('custom-time-fields-end');
if (dateRange === 'custom') {
customDateFields.style.display = 'block';
customTimeFields.style.display = 'block';
customDateFieldsEnd.style.display = 'block';
customTimeFieldsEnd.style.display = 'block';
} else {
customDateFields.style.display = 'none';
customTimeFields.style.display = 'none';
customDateFieldsEnd.style.display = 'none';
customTimeFieldsEnd.style.display = 'none';
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
toggleCustomDateRange();
});
</script>
{% 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