Update reports

parent e3eef4f3
# Reports Sync Implementation Guide
## Overview
This document describes the implementation of the reports sync API endpoint and web interface according to the Reports Synchronization Protocol Documentation.
## Changes Made
### 1. Database Model Updates
#### ReportSync Model (`app/models.py`)
- **Added**: `cap_compensation_balance` field to track the CAP redistribution balance at the time of sync
- **Type**: `NUMERIC(15, 2)` with default value 0.00
- **Purpose**: Stores the accumulated shortfall from cap compensation system for historical tracking
#### Updated `to_dict()` method
- Now includes `cap_compensation_balance` in JSON serialization
### 2. API Endpoint Updates (`app/api/routes.py`)
#### `/api/reports/sync` Endpoint
**New Features:**
1. **Cap Compensation Balance Handling**
- Accepts `cap_compensation_balance` field in request payload
- Stores the value in the ReportSync record
- Tracks historical CAP balance over time
2. **Differentiation Between New and Updated Records**
- **Bets**:
- Checks for existing bet UUIDs
- Updates existing bets if fields have changed (total_amount, paid, paid_out)
- Tracks `bets_new` and `bets_duplicate` counts
- Skips unchanged duplicate bets
- **Extraction Stats**:
- Checks for existing stats by match_id + client_id
- Updates existing stats with new data
- Tracks `stats_new` and `stats_updated` counts
3. **Enhanced Logging**
- `ReportSyncLog` now tracks:
- `bets_new`: Number of new bets added
- `bets_duplicate`: Number of duplicate bets (updated or skipped)
- `stats_new`: Number of new extraction stats
- `stats_updated`: Number of updated extraction stats
**Request Payload Example:**
```json
{
"sync_id": "sync_20260201_214327_abc12345",
"client_id": "client_unique_identifier",
"sync_timestamp": "2026-02-01T21:43:27.249Z",
"date_range": "all",
"start_date": "2026-01-01T00:00:00",
"end_date": "2026-02-01T21:43:27.249Z",
"bets": [...],
"extraction_stats": [...],
"cap_compensation_balance": 5000.0,
"summary": {...},
"is_incremental": true,
"sync_type": "incremental"
}
```
### 3. Web Interface Updates
#### Reports Page (`app/templates/main/reports.html`)
**New Features:**
1. **Enhanced Date Range Filters**
- Today (default)
- Yesterday
- This Week
- Last Week
- This Month
- All Time
- Custom (with date and time selection)
2. **Custom Date/Time Selection**
- Start Date and Start Time (optional)
- End Date and End Time (optional)
- JavaScript toggles visibility when "Custom" is selected
3. **Client Selection Dropdown**
- Shows all clients with their token names
- Format: "Token Name (client_id)"
- Filters reports by selected client
4. **Enhanced Report Table**
- Added "Balance" column (payin - payout)
- Added "CAP Balance" column showing cap_compensation_balance
- Color-coded balance (green for positive, red for negative)
- CAP balance shown in blue for visibility
**Filter Logic:**
- **Today**: From 00:00:00 today to now
- **Yesterday**: From 00:00:00 to 23:59:59 yesterday
- **This Week**: From Monday 00:00:00 to now
- **Last Week**: From Monday 00:00:00 to Sunday 23:59:59
- **This Month**: From 1st 00:00:00 to now
- **All**: No date filtering
- **Custom**: User-specified date and time range
#### Report Detail Page (`app/templates/main/report_detail.html`)
**New Features:**
1. **Enhanced Summary Section**
- Added "CAP Redistribution Balance" card
- Shows balance at the time of the sync
- Displayed in yellow for visibility
- Helps track CAP balance over time
2. **Match-Level Data Display**
- **Bets Tab**: Shows all bets with expandable details
- Bet UUID, Fixture ID, DateTime, Amount, Count
- Paid/Paid Out status indicators
- Expandable bet details with match-level information
- Match ID, Match Number, Outcome, Amount, Win Amount, Result
- **Extraction Stats Tab**: Shows all extraction stats with expandable details
- Match ID, Fixture ID, DateTime, Bets, Collected, Redistributed
- Actual Result, Extraction Result, CAP Applied status
- Expandable details showing:
- Under/Over bets and amounts
- Result breakdown by outcome
### 4. Backend Route Updates (`app/main/routes.py`)
#### `/reports` Route
**New Features:**
1. **Date Range Calculation**
- Implements logic for all date range filters
- Handles week calculations (Monday start)
- Supports custom date/time ranges
2. **Client Data Aggregation**
- Joins ReportSync with ClientActivity and APIToken
- Provides client_id and token_name for dropdown
- Filters by user's tokens for non-admin users
3. **Enhanced Filtering**
- Applies date range filters to query
- Supports client_id filtering
- Maintains pagination and sorting
## Database Migration
### Automatic Migration System
The application uses an automatic migration system that runs all pending migrations at application launch. The migration to add `cap_compensation_balance` is already integrated into this system.
**Migration Class**: `Migration_011_AddCapCompensationBalance` in [`app/database/migrations.py`](app/database/migrations.py:777)
**What it does:**
1. Checks if `report_syncs` table exists
2. Checks if `cap_compensation_balance` column already exists
3. Adds `cap_compensation_balance DECIMAL(15,2) NOT NULL DEFAULT 0.00` if not present
4. Provides rollback capability (downgrade function)
**Automatic Execution:**
- Migrations run automatically when the application starts (see [`app/__init__.py`](app/__init__.py:68-77))
- The migration system tracks applied migrations in the `database_versions` table
- Only pending migrations are executed on each startup
- Migration status is logged to the application log
**Manual Execution (if needed):**
```bash
# The migration will run automatically on app startup
# To manually trigger migrations:
python -c "from app.database import run_migrations; print(run_migrations())"
```
**Upgrade SQL:**
```sql
ALTER TABLE report_syncs
ADD COLUMN cap_compensation_balance DECIMAL(15,2) NOT NULL DEFAULT 0.00
```
**Downgrade SQL:**
```sql
ALTER TABLE report_syncs
DROP COLUMN IF EXISTS cap_compensation_balance
```
## Usage Examples
### 1. Automatic Migration Execution
The migration will run automatically when you start the application. No manual intervention is required.
**Expected log output on first run:**
```
INFO: Applying migration 011: Add cap_compensation_balance column to report_syncs table
INFO: Added cap_compensation_balance column successfully
INFO: Migration 011 applied successfully
INFO: Database migrations completed: Successfully applied 1 migrations
```
**Expected log output on subsequent runs:**
```
INFO: Migration 011 already applied, skipping
INFO: No pending migrations
```
**To verify migration was applied:**
```bash
# Check application logs
tail -f logs/fixture-daemon.log | grep migration
# Or check database directly
mysql -u your_user -p your_database -e "DESCRIBE report_syncs;" | grep cap_compensation_balance
```
### 2. Syncing Reports from Client
```bash
curl -X POST http://your-server/api/reports/sync \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-d '{
"sync_id": "sync_20260201_214327_abc12345",
"client_id": "client_machine_001",
"sync_timestamp": "2026-02-01T21:43:27.249Z",
"date_range": "all",
"start_date": "2026-01-01T00:00:00",
"end_date": "2026-02-01T21:43:27.249Z",
"bets": [
{
"uuid": "bet-uuid-12345",
"fixture_id": "fixture-20260201",
"bet_datetime": "2026-02-01T10:30:00",
"paid": true,
"paid_out": false,
"total_amount": 5000.0,
"bet_count": 2,
"details": [...]
}
],
"extraction_stats": [...],
"cap_compensation_balance": 5000.0,
"summary": {
"total_payin": 100000.0,
"total_payout": 95000.0,
"net_profit": 5000.0,
"total_bets": 50,
"total_matches": 1
},
"is_incremental": true,
"sync_type": "incremental"
}'
```
### 3. Viewing Reports in Web Interface
1. Navigate to `/reports`
2. Use filters to narrow down data:
- Select a client from dropdown
- Choose date range (Today, Yesterday, This Week, etc.)
- For custom range, select dates and optional times
3. Click "Apply Filters"
4. View aggregated data:
- Total Payin, Payout, Balance
- CAP Redistribution Balance
- Total Bets and Matches
5. Click "View" on any report to see details
### 4. Viewing Report Details
1. Click "View" on any report in the list
2. See summary with CAP balance at sync time
3. Switch between tabs:
- **Bets**: View all bets with match-level details
- **Extraction Stats**: View extraction statistics with breakdowns
4. Expand rows to see detailed information
## Key Features
### 1. Historical CAP Balance Tracking
- CAP balance is stored with each sync
- Shows balance at the time of the report
- Enables tracking CAP balance changes over time
- Displayed prominently in reports and details
### 2. Record Differentiation
- **New Records**: First-time sync of a bet or extraction stat
- **Updated Records**: Existing records with changed data
- **Duplicate Records**: Existing records without changes (skipped)
- All tracked in sync logs for audit trail
### 3. Flexible Date Filtering
- Predefined ranges for common use cases
- Custom range with optional time precision
- JavaScript toggles custom fields visibility
- Default to "Today" for immediate relevance
### 4. Client-Centric View
- Filter by specific client
- Shows token names for easy identification
- Aggregates data by client
- Supports multi-client environments
### 5. Match-Level Detail
- Expandable bet details showing individual match bets
- Extraction stats with result breakdowns
- Under/Over bet distribution
- Complete audit trail for each match
## Data Flow
### Sync Process
1. Client sends sync request with all data
2. Server validates request and authenticates
3. Server checks for duplicate sync_id (idempotency)
4. For each bet:
- Check if UUID exists
- If exists and changed: Update
- If exists and unchanged: Skip
- If new: Create
5. For each extraction stat:
- Check if match_id + client_id exists
- If exists: Update
- If new: Create
6. Store cap_compensation_balance with sync
7. Log operation details
8. Return success response
### Report Viewing Process
1. User navigates to reports page
2. Applies filters (client, date range, etc.)
3. Server queries database with filters
4. Aggregates data by client
5. Displays in table with CAP balance
6. User clicks "View" for details
7. Server retrieves all bets and stats for sync
8. Displays with expandable match-level details
## Testing
### 1. Test Automatic Migration
```bash
# Start the application - migration will run automatically
python main.py
```
Check logs for migration status:
```bash
tail -f logs/fixture-daemon.log | grep -i migration
```
Verify column exists in database:
```sql
DESCRIBE report_syncs;
```
### 2. Test API Endpoint
```bash
# Test with cap_compensation_balance
curl -X POST http://localhost:5000/api/reports/sync \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d @test_sync_payload.json
```
### 3. Test Web Interface
1. Start application
2. Navigate to `/reports`
3. Test each date range filter
4. Test custom date/time selection
5. Test client filtering
6. Verify CAP balance display
7. Click through to detail pages
8. Verify match-level data display
### 4. Test Record Updates
1. Send initial sync with bet data
2. Send updated sync with same bet UUID but different amount
3. Verify bet was updated (not duplicated)
4. Check sync logs for update tracking
## Troubleshooting
### Migration Issues
- **Error**: Column already exists
- **Solution**: Migration checks for this, safe to ignore
- **Error**: Permission denied
- **Solution**: Run with database user that has ALTER TABLE privileges
### API Issues
- **Error**: Invalid UUID format
- **Solution**: Ensure UUIDs are valid UUID v4 format
- **Error**: Duplicate sync_id
- **Solution**: This is expected behavior (idempotency)
- **Error**: Missing cap_compensation_balance
- **Solution**: Field is optional, defaults to 0.00
### Web Interface Issues
- **Issue**: Custom date fields not showing
- **Solution**: Check JavaScript console for errors, ensure Bootstrap loaded
- **Issue**: Client dropdown empty
- **Solution**: Ensure clients have synced data and tokens are active
- **Issue**: CAP balance not displaying
- **Solution**: Run migration to add column, check database schema
## Security Considerations
1. **Authentication**: All API endpoints require valid API token
2. **Authorization**: Non-admin users only see their own clients' data
3. **Input Validation**: All datetime fields validated before processing
4. **SQL Injection**: Uses SQLAlchemy ORM, parameterized queries
5. **XSS Protection**: Templates use Jinja2 auto-escaping
## Performance Considerations
1. **Indexing**:
- `sync_id` indexed for duplicate detection
- `client_id` indexed for filtering
- `sync_timestamp` indexed for date range queries
- `uuid` indexed in bets table for lookups
2. **Pagination**:
- Reports list paginated (default 20 per page)
- Prevents loading large datasets at once
3. **Query Optimization**:
- Uses JOINs efficiently
- Filters applied at database level
- Only retrieves necessary columns
## Future Enhancements
1. **Real-time Updates**: WebSocket for live sync status
2. **Charts**: Visual representation of CAP balance over time
3. **Export Enhancements**: More export formats and customization
4. **Advanced Filtering**: Filter by match status, outcome, etc.
5. **Bulk Actions**: Bulk export, delete, or archive operations
## References
- [Reports Synchronization Protocol Documentation](REPORTS_SYNC_PROTOCOL_DOCUMENTATION.md)
- [API Routes](app/api/routes.py)
- [Main Routes](app/main/routes.py)
- [Database Models](app/models.py)
## Support
For issues or questions:
1. Check this documentation
2. Review protocol documentation
3. Check application logs
4. Verify database schema
5. Test with sample data
\ No newline at end of file
# Reports Synchronization Protocol Documentation
## Overview
The reports synchronization protocol sends betting and extraction statistics data from the client to the server. The system uses **incremental synchronization** - only new or updated records are sent after the initial full sync.
**IMPORTANT**: The system now syncs ALL reports (not just today's data) and includes cap compensation balance information.
---
## API Endpoint
- **URL**: `/api/reports/sync`
- **Method**: `POST`
- **Content-Type**: `application/json`
- **Authentication**: Bearer token (if configured)
- **Interval**: 10 minutes (configurable)
---
## Request Payload Structure
```json
{
"sync_id": "sync_20260201_214327_abc12345",
"client_id": "client_unique_identifier",
"sync_timestamp": "2026-02-01T21:43:27.249Z",
"date_range": "all",
"start_date": "2026-01-01T00:00:00",
"end_date": "2026-02-01T21:43:27.249Z",
"bets": [...],
"extraction_stats": [...],
"cap_compensation_balance": 5000.0,
"summary": {...},
"is_incremental": true,
"sync_type": "incremental"
}
```
---
## Request Fields
### Metadata Fields
| Field | Type | Description |
|-------|------|-------------|
| `sync_id` | String | Unique identifier for this sync operation |
| `client_id` | String | Unique client identifier (machine ID or rustdesk_id) |
| `sync_timestamp` | ISO 8601 DateTime | When the sync was initiated |
| `date_range` | String | Date range for sync: "all", "today", "yesterday", "week" |
| `start_date` | ISO 8601 DateTime | Start of date range |
| `end_date` | ISO 8601 DateTime | End of date range |
| `is_incremental` | Boolean | True if this is an incremental sync (only new/changed data) |
| `sync_type` | String | "full" for first sync, "incremental" for subsequent syncs |
### Cap Compensation Balance
| Field | Type | Description |
|-------|------|-------------|
| `cap_compensation_balance` | Float | Accumulated shortfall from cap compensation system |
**Note**: This field represents the current balance of cap compensation adjustments. It's the `accumulated_shortfall` value from the `PersistentRedistributionAdjustmentModel` table, which tracks adjustments across all extractions.
---
## Bet Data Structure
Each bet in the `bets` array contains:
```json
{
"uuid": "bet-uuid-here",
"fixture_id": "fixture-123",
"bet_datetime": "2026-02-01T10:30:00",
"paid": true,
"paid_out": false,
"total_amount": 5000.0,
"bet_count": 3,
"details": [
{
"match_id": 123,
"match_number": "MATCH001",
"outcome": "WIN1",
"amount": 2000.0,
"win_amount": 0.0,
"result": "pending"
}
]
}
```
### Bet Fields
| Field | Type | Description |
|-------|------|-------------|
| `uuid` | String | Unique bet identifier |
| `fixture_id` | String | Fixture identifier from matches table |
| `bet_datetime` | ISO 8601 DateTime | When the bet was placed |
| `paid` | Boolean | Whether payment was received |
| `paid_out` | Boolean | Whether winnings were paid out |
| `total_amount` | Float | Sum of all bet detail amounts |
| `bet_count` | Integer | Number of bet details |
| `details` | Array | Array of bet detail objects |
### Bet Detail Fields
| Field | Type | Description |
|-------|------|-------------|
| `match_id` | Integer | Match ID from matches table |
| `match_number` | Integer | Match number for display |
| `outcome` | String | Bet outcome/prediction (e.g., "WIN1", "DRAW", "X") |
| `amount` | Float | Bet amount |
| `win_amount` | Float | Winning amount (0.0 if not won) |
| `result` | String | Result status: "win", "lost", "pending", "cancelled" |
**Important**: Only bets with non-cancelled details are included in the sync.
---
## Extraction Stats Structure
Each stat in the `extraction_stats` array contains:
```json
{
"match_id": 123,
"fixture_id": "fixture-123",
"match_datetime": "2026-02-01T12:00:00",
"total_bets": 50,
"total_amount_collected": 100000.0,
"total_redistributed": 95000.0,
"actual_result": "WIN1",
"extraction_result": "WIN1",
"cap_applied": true,
"cap_percentage": 5.0,
"under_bets": 20,
"under_amount": 40000.0,
"over_bets": 30,
"over_amount": 60000.0,
"result_breakdown": {
"WIN1": {"bets": 10, "amount": 20000.0},
"DRAW": {"bets": 5, "amount": 10000.0},
"WIN2": {"bets": 35, "amount": 70000.0}
}
}
```
### Extraction Stats Fields
| Field | Type | Description |
|-------|------|-------------|
| `match_id` | Integer | Match ID from matches table |
| `fixture_id` | String | Fixture identifier |
| `match_datetime` | ISO 8601 DateTime | When the match was completed |
| `total_bets` | Integer | Total number of bets on this match |
| `total_amount_collected` | Float | Total amount collected from all bets |
| `total_redistributed` | Float | Total amount redistributed to winners |
| `actual_result` | String | The actual match result |
| `extraction_result` | String | Result from extraction system (if different) |
| `cap_applied` | Boolean | Whether redistribution CAP was applied |
| `cap_percentage` | Float | CAP percentage used (if applied) |
| `under_bets` | Integer | Number of UNDER bets |
| `under_amount` | Float | Total amount bet on UNDER |
| `over_bets` | Integer | Number of OVER bets |
| `over_amount` | Float | Total amount bet on OVER |
| `result_breakdown` | JSON | Detailed breakdown by result option |
---
## Summary Structure
```json
{
"total_payin": 100000.0,
"total_payout": 95000.0,
"net_profit": 5000.0,
"total_bets": 50,
"total_matches": 10
}
```
### Summary Fields
| Field | Type | Description |
|-------|------|-------------|
| `total_payin` | Float | Total amount collected from bets |
| `total_payout` | Float | Total amount redistributed |
| `net_profit` | Float | Net profit (payin - payout) |
| `total_bets` | Integer | Total number of bets |
| `total_matches` | Integer | Total number of matches |
---
## Incremental Synchronization Logic
The system uses `ReportsSyncTrackingModel` to track what has been synced:
### First Sync (Full Sync)
- No previous sync record exists
- All bets and extraction stats are sent
- `sync_type: "full"`
- `date_range: "all"` (sends all historical data)
### Subsequent Syncs (Incremental)
- Only records updated since `last_synced_at` are sent
- For each bet: checks if `bet.updated_at > tracking.last_synced_at`
- For each stat: checks if `stat.updated_at > tracking.last_synced_at`
- `sync_type: "incremental"`
- `date_range: "all"` (but only includes new/changed records)
### Tracking Records
The client maintains tracking records for:
- **Sync operations**: `entity_type='sync'`, `entity_id='latest'`
- **Individual bets**: `entity_type='bet'`, `entity_id=bet.uuid`
- **Extraction stats**: `entity_type='extraction_stat'`, `entity_id=match_id`
---
## Server Response Format
### Success Response
```json
{
"success": true,
"synced_count": 25,
"message": "Successfully synced 25 items"
}
```
### Error Response
```json
{
"success": false,
"error": "Error message here"
}
```
---
## Data Completeness for Report Recreation
The protocol sends all necessary data to recreate the same reports on the server:
### Complete Information Includes:
1. **Bet Information**: Complete bet details including amounts, outcomes, results
2. **Match Information**: Match IDs and numbers linked to bet details
3. **Extraction Statistics**: Complete extraction data including caps, amounts, results
4. **Cap Compensation Balance**: Current accumulated shortfall for cap compensation
5. **Timestamps**: All datetime fields for accurate reporting
6. **Financial Data**: Payin, payout, redistribution amounts
### Server Can Use This Data To:
- Calculate daily/weekly/monthly summaries
- Generate match-by-match reports
- Track winning/losing bets
- Calculate profit/loss
- Apply the same extraction logic
- Track cap compensation adjustments
- Reconcile accumulated shortfall across all extractions
---
## Retry and Queue System
The client includes a robust retry mechanism:
### Queue Management
- **Queue Model**: `ReportsSyncQueueModel`
- **Max Queue Size**: 1000 items
- **Max Retries**: 5 attempts
- **Backoff Strategy**: Exponential backoff (60s * 2^retry_count)
### Queue Status
- `pending`: Waiting to be synced
- `syncing`: Currently being synced
- `completed`: Successfully synced
- `failed`: Failed after max retries
### Retry Logic
1. Failed syncs are queued for retry
2. Exponential backoff between retries
3. Oldest completed items are removed when queue is full
4. Failed items are re-queued when server becomes available
---
## Implementation Notes for Server Developers
### 1. Handling Incremental Syncs
- Check `sync_type` field to determine if full or incremental
- For incremental syncs, only process new/updated records
- Use `sync_id` for tracking and deduplication
### 2. Cap Compensation Balance
- The `cap_compensation_balance` field represents the current accumulated shortfall
- This value should be stored and used for reconciliation
- It tracks adjustments across all extractions
### 3. Data Validation
- Validate all required fields are present
- Check UUIDs are unique
- Verify match IDs exist in your database
- Validate datetime formats
### 4. Error Handling
- Return appropriate HTTP status codes
- Provide clear error messages
- Log sync failures for debugging
### 5. Performance Considerations
- Process large payloads in batches if needed
- Use database transactions for data integrity
- Implement idempotent operations for retry safety
### 6. Security
- Validate Bearer token authentication
- Verify client_id matches expected clients
- Rate limit sync requests if necessary
---
## Example Full Request
```json
{
"sync_id": "sync_20260201_214327_abc12345",
"client_id": "machine_hostname_1234567890",
"sync_timestamp": "2026-02-01T21:43:27.249Z",
"date_range": "all",
"start_date": "2026-01-01T00:00:00",
"end_date": "2026-02-01T21:43:27.249Z",
"bets": [
{
"uuid": "bet-uuid-12345",
"fixture_id": "fixture-20260201",
"bet_datetime": "2026-02-01T10:30:00",
"paid": true,
"paid_out": false,
"total_amount": 5000.0,
"bet_count": 2,
"details": [
{
"match_id": 123,
"match_number": 1,
"outcome": "WIN1",
"amount": 3000.0,
"win_amount": 0.0,
"result": "pending"
},
{
"match_id": 124,
"match_number": 2,
"outcome": "DRAW",
"amount": 2000.0,
"win_amount": 0.0,
"result": "pending"
}
]
}
],
"extraction_stats": [
{
"match_id": 123,
"fixture_id": "fixture-20260201",
"match_datetime": "2026-02-01T12:00:00",
"total_bets": 50,
"total_amount_collected": 100000.0,
"total_redistributed": 95000.0,
"actual_result": "WIN1",
"extraction_result": "WIN1",
"cap_applied": true,
"cap_percentage": 5.0,
"under_bets": 20,
"under_amount": 40000.0,
"over_bets": 30,
"over_amount": 60000.0,
"result_breakdown": {
"WIN1": {"bets": 10, "amount": 20000.0},
"DRAW": {"bets": 5, "amount": 10000.0},
"WIN2": {"bets": 35, "amount": 70000.0}
}
}
],
"cap_compensation_balance": 5000.0,
"summary": {
"total_payin": 100000.0,
"total_payout": 95000.0,
"net_profit": 5000.0,
"total_bets": 50,
"total_matches": 1
},
"is_incremental": true,
"sync_type": "incremental"
}
```
---
## Summary
The reports sync protocol provides:
✅ Complete data for report recreation
✅ Incremental sync (new/updated records only)
✅ Tracking of synced entities
✅ Retry mechanism for failed syncs
✅ Syncs ALL reports (not just today)
✅ Includes cap compensation balance
✅ Robust queue management
✅ Exponential backoff for retries
The server can use this data to recreate all reports, track cap compensation adjustments, and maintain accurate financial records across all historical data.
\ No newline at end of file
...@@ -1041,6 +1041,9 @@ def api_reports_sync(): ...@@ -1041,6 +1041,9 @@ def api_reports_sync():
'details': f'Missing required field: {field}' 'details': f'Missing required field: {field}'
}), 400 }), 400
# Get cap compensation balance (optional field)
cap_compensation_balance = data.get('cap_compensation_balance', 0.00)
# Check for duplicate sync_id (idempotency) # Check for duplicate sync_id (idempotency)
existing_sync = ReportSync.query.filter_by(sync_id=data['sync_id']).first() existing_sync = ReportSync.query.filter_by(sync_id=data['sync_id']).first()
if existing_sync: if existing_sync:
...@@ -1111,12 +1114,16 @@ def api_reports_sync(): ...@@ -1111,12 +1114,16 @@ def api_reports_sync():
total_payout=summary['total_payout'], total_payout=summary['total_payout'],
net_profit=summary['net_profit'], net_profit=summary['net_profit'],
total_bets=summary['total_bets'], 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) db.session.add(report_sync)
# Process bets # Process bets
bets_count = 0 bets_count = 0
bets_new = 0
bets_duplicate = 0
for bet_data in data['bets']: for bet_data in data['bets']:
# Validate bet UUID # Validate bet UUID
try: try:
...@@ -1131,7 +1138,26 @@ def api_reports_sync(): ...@@ -1131,7 +1138,26 @@ def api_reports_sync():
# Check for duplicate bet UUID # Check for duplicate bet UUID
existing_bet = Bet.query.filter_by(uuid=bet_data['uuid']).first() existing_bet = Bet.query.filter_by(uuid=bet_data['uuid']).first()
if existing_bet: 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 continue
# Parse bet datetime # Parse bet datetime
...@@ -1160,6 +1186,7 @@ def api_reports_sync(): ...@@ -1160,6 +1186,7 @@ def api_reports_sync():
# Flush to get bet.id before creating bet details # Flush to get bet.id before creating bet details
db.session.flush() db.session.flush()
bets_count += 1 bets_count += 1
bets_new += 1
# Process bet details # Process bet details
for detail_data in bet_data.get('details', []): for detail_data in bet_data.get('details', []):
...@@ -1250,8 +1277,8 @@ def api_reports_sync(): ...@@ -1250,8 +1277,8 @@ def api_reports_sync():
operation_type='new_sync', operation_type='new_sync',
status='success', status='success',
bets_processed=bets_count, bets_processed=bets_count,
bets_new=bets_count, bets_new=bets_new,
bets_duplicate=0, bets_duplicate=bets_duplicate,
stats_processed=stats_count, stats_processed=stats_count,
stats_new=stats_new, stats_new=stats_new,
stats_updated=stats_updated, stats_updated=stats_updated,
......
...@@ -775,6 +775,61 @@ class Migration_010_CreateReportsTables(Migration): ...@@ -775,6 +775,61 @@ class Migration_010_CreateReportsTables(Migration):
def can_rollback(self) -> bool: def can_rollback(self) -> bool:
return True 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: class MigrationManager:
"""Manages database migrations and versioning""" """Manages database migrations and versioning"""
...@@ -790,6 +845,7 @@ class MigrationManager: ...@@ -790,6 +845,7 @@ class MigrationManager:
Migration_008_AddRemoteDomainSetting(), Migration_008_AddRemoteDomainSetting(),
Migration_009_CreateClientActivityTable(), Migration_009_CreateClientActivityTable(),
Migration_010_CreateReportsTables(), Migration_010_CreateReportsTables(),
Migration_011_AddCapCompensationBalance(),
] ]
def ensure_version_table(self): 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(): ...@@ -1541,14 +1541,16 @@ def clients():
def reports(): def reports():
"""Reports page with filtering, pagination, and export""" """Reports page with filtering, pagination, and export"""
try: try:
from app.models import ReportSync, Bet, ExtractionStats from app.models import ReportSync, Bet, ExtractionStats, APIToken, ClientActivity
from sqlalchemy import func, and_, or_ from sqlalchemy import func, and_, or_
# Get filter parameters # Get filter parameters
client_id_filter = request.args.get('client_id', '').strip() 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() start_date_filter = request.args.get('start_date', '').strip()
end_date_filter = request.args.get('end_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_by = request.args.get('sort_by', 'sync_timestamp')
sort_order = request.args.get('sort_order', 'desc') sort_order = request.args.get('sort_order', 'desc')
export_format = request.args.get('export', '').strip() export_format = request.args.get('export', '').strip()
...@@ -1557,16 +1559,66 @@ def reports(): ...@@ -1557,16 +1559,66 @@ def reports():
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', 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 # Base query
if current_user.is_admin: if current_user.is_admin:
query = ReportSync.query query = ReportSync.query
else: else:
# Non-admin users can only see reports from their own clients # 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()] user_token_ids = [t.id for t in APIToken.query.filter_by(user_id=current_user.id).all()]
if user_token_ids: if user_token_ids:
# Get client_ids from ClientActivity for this user's tokens # 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( client_ids = [c.rustdesk_id for c in ClientActivity.query.filter(
ClientActivity.api_token_id.in_(user_token_ids) ClientActivity.api_token_id.in_(user_token_ids)
).all()] ).all()]
...@@ -1581,21 +1633,11 @@ def reports(): ...@@ -1581,21 +1633,11 @@ def reports():
if client_id_filter: if client_id_filter:
query = query.filter(ReportSync.client_id == client_id_filter) query = query.filter(ReportSync.client_id == client_id_filter)
if start_date_filter: if start_date:
try: query = query.filter(ReportSync.start_date >= start_date)
start_date = datetime.strptime(start_date_filter, '%Y-%m-%d')
query = query.filter(ReportSync.start_date >= start_date)
except ValueError:
pass
if end_date_filter: if end_date:
try: query = query.filter(ReportSync.end_date <= end_date)
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)
query = query.filter(ReportSync.end_date <= end_date)
except ValueError:
pass
# Sorting # Sorting
if hasattr(ReportSync, sort_by): if hasattr(ReportSync, sort_by):
...@@ -1614,29 +1656,48 @@ def reports(): ...@@ -1614,29 +1656,48 @@ def reports():
# Pagination # Pagination
reports_pagination = query.paginate(page=page, per_page=per_page, error_out=False) 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: 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: else:
if user_token_ids: if user_token_ids:
from app.models import ClientActivity clients_query = db.session.query(
all_client_ids = db.session.query(ClientActivity.rustdesk_id).filter( ClientActivity.rustdesk_id.label('client_id'),
ClientActivity.api_token_id.in_(user_token_ids) 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() ).distinct().all()
client_data = [{'client_id': c.client_id, 'token_name': c.token_name} for c in clients_query]
else: else:
all_client_ids = [] client_data = []
client_ids = [cid[0] for cid in all_client_ids if cid[0]]
return render_template('main/reports.html', return render_template('main/reports.html',
reports=reports_pagination.items, reports=reports_pagination.items,
pagination=reports_pagination, pagination=reports_pagination,
client_ids=client_ids, client_data=client_data,
filters={ filters={
'client_id': client_id_filter, 'client_id': client_id_filter,
'date_range': date_range_filter, 'date_range': date_range_filter,
'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,
'end_time': end_time_filter,
'sort_by': sort_by, 'sort_by': sort_by,
'sort_order': sort_order 'sort_order': sort_order
}) })
......
...@@ -857,6 +857,9 @@ class ReportSync(db.Model): ...@@ -857,6 +857,9 @@ class ReportSync(db.Model):
total_bets = db.Column(db.Integer, default=0) total_bets = db.Column(db.Integer, default=0)
total_matches = 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 # Metadata
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
...@@ -879,6 +882,7 @@ class ReportSync(db.Model): ...@@ -879,6 +882,7 @@ class ReportSync(db.Model):
'net_profit': float(self.net_profit) if self.net_profit else 0.0, 'net_profit': float(self.net_profit) if self.net_profit else 0.0,
'total_bets': self.total_bets, 'total_bets': self.total_bets,
'total_matches': self.total_matches, '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 'created_at': self.created_at.isoformat() if self.created_at else None
} }
......
...@@ -80,22 +80,28 @@ ...@@ -80,22 +80,28 @@
</div> </div>
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col-md-4"> <div class="col-md-3">
<div class="p-3 bg-primary text-white rounded"> <div class="p-3 bg-primary text-white rounded">
<h6 class="mb-1">Total Payin</h6> <h6 class="mb-1">Total Payin</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.total_payin) if report.total_payin else '0.00' }}</h3> <h3 class="mb-0">{{ "{:,.2f}".format(report.total_payin) if report.total_payin else '0.00' }}</h3>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="p-3 bg-info text-white rounded"> <div class="p-3 bg-info text-white rounded">
<h6 class="mb-1">Total Payout</h6> <h6 class="mb-1">Total Payout</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.total_payout) if report.total_payout else '0.00' }}</h3> <h3 class="mb-0">{{ "{:,.2f}".format(report.total_payout) if report.total_payout else '0.00' }}</h3>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<div class="p-3 {% if report.net_profit >= 0 %}bg-success{% else %}bg-danger{% endif %} text-white rounded"> <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">Net Profit</h6> <h6 class="mb-1">Balance</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(report.net_profit) if report.net_profit else '0.00' }}</h3> <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> </div>
</div> </div>
...@@ -189,7 +195,7 @@ ...@@ -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.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 class="text-end">{{ "{:,.2f}".format(detail.win_amount) if detail.win_amount else '0.00' }}</td>
<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 }} {{ detail.result }}
</span> </span>
</td> </td>
...@@ -295,7 +301,6 @@ ...@@ -295,7 +301,6 @@
<th>Outcome</th> <th>Outcome</th>
<th>Bets</th> <th>Bets</th>
<th>Amount</th> <th>Amount</th>
<th>Coefficient</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
...@@ -304,7 +309,6 @@ ...@@ -304,7 +309,6 @@
<td>{{ outcome }}</td> <td>{{ outcome }}</td>
<td>{{ data.bets }}</td> <td>{{ data.bets }}</td>
<td>{{ "{:,.2f}".format(data.amount) if data.amount else '0.00' }}</td> <td>{{ "{:,.2f}".format(data.amount) if data.amount else '0.00' }}</td>
<td>{{ data.coefficient }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
...@@ -28,33 +28,43 @@ ...@@ -28,33 +28,43 @@
<div class="card-body"> <div class="card-body">
<form method="GET" action="{{ url_for('main.reports') }}" class="row g-3"> <form method="GET" action="{{ url_for('main.reports') }}" class="row g-3">
<div class="col-md-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"> <select class="form-select" id="client_id" name="client_id">
<option value="">All Clients</option> <option value="">All Clients</option>
{% for cid in client_ids %} {% for client in client_data %}
<option value="{{ cid }}" {% if filters.client_id == cid %}selected{% endif %}>{{ cid }}</option> <option value="{{ client.client_id }}" {% if filters.client_id == client.client_id %}selected{% endif %}>{{ client.token_name }} ({{ client.client_id }})</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="date_range" class="form-label">Date Range</label> <label for="date_range" class="form-label">Date Range</label>
<select class="form-select" id="date_range" name="date_range"> <select class="form-select" id="date_range" name="date_range" onchange="toggleCustomDateRange()">
<option value="">All</option>
<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>
<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="all" {% if filters.date_range == 'all' %}selected{% endif %}>All Time</option>
<option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>Custom</option>
</select> </select>
</div> </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> <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 }}"> <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-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> <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 }}"> <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-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> <label for="sort_by" class="form-label">Sort By</label>
<select class="form-select" id="sort_by" name="sort_by"> <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> <option value="sync_timestamp" {% if filters.sort_by == 'sync_timestamp' %}selected{% endif %}>Sync Timestamp</option>
...@@ -93,12 +103,13 @@ ...@@ -93,12 +103,13 @@
<thead> <thead>
<tr> <tr>
<th>Sync ID</th> <th>Sync ID</th>
<th>Client ID</th> <th>Client</th>
<th>Sync Timestamp</th> <th>Sync Timestamp</th>
<th>Date Range</th> <th>Date Range</th>
<th>Total Payin</th> <th>Total Payin</th>
<th>Total Payout</th> <th>Total Payout</th>
<th>Net Profit</th> <th>Balance</th>
<th>CAP Balance</th>
<th>Total Bets</th> <th>Total Bets</th>
<th>Total Matches</th> <th>Total Matches</th>
<th>Actions</th> <th>Actions</th>
...@@ -113,8 +124,11 @@ ...@@ -113,8 +124,11 @@
<td>{{ report.date_range }}</td> <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_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">{{ "{:,.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 %}"> <td class="text-end {% if (report.total_payin - report.total_payout) >= 0 %}text-success{% else %}text-danger{% endif %}">
{{ "{:,.2f}".format(report.net_profit) if report.net_profit else '0.00' }} {{ "{:,.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>
<td class="text-center">{{ report.total_bets }}</td> <td class="text-center">{{ report.total_bets }}</td>
<td class="text-center">{{ report.total_matches }}</td> <td class="text-center">{{ report.total_matches }}</td>
...@@ -178,4 +192,31 @@ ...@@ -178,4 +192,31 @@
</div> </div>
</div> </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 %} {% 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