New limits CAP

parent 2d93850e
...@@ -189,14 +189,16 @@ Store comprehensive extraction metrics in `extraction_stats` table: ...@@ -189,14 +189,16 @@ Store comprehensive extraction metrics in `extraction_stats` table:
- CAP applied status - CAP applied status
- Result breakdown - Result breakdown
#### Step 6.3: Shortfall Tracking #### Step 6.3: Redistribution Adjustment Tracking
**Expected Redistribution = Total Payin × CAP Percentage** **Expected Redistribution = Total Payin × CAP Percentage**
**Actual Redistribution = Sum of all win_amounts** **Actual Redistribution = Sum of all win_amounts**
**Shortfall = max(0, Expected - Actual)** **Adjustment = Expected - Actual**
- Positive: Under-redistribution (shortfall) - accumulated to increase future CAP
- Negative: Over-redistribution (surplus) - accumulated to decrease future CAP
Update daily shortfall tracking for future CAP adjustments. Update daily redistribution adjustment tracking for future CAP adjustments.
### Phase 7: System Notifications ### Phase 7: System Notifications
...@@ -216,7 +218,7 @@ Send `MATCH_DONE` message to advance to next match. ...@@ -216,7 +218,7 @@ Send `MATCH_DONE` message to advance to next match.
- **Purpose**: Prevent excessive payouts that could harm profitability - **Purpose**: Prevent excessive payouts that could harm profitability
- **Mechanism**: Only select results where total payout ≤ CAP threshold - **Mechanism**: Only select results where total payout ≤ CAP threshold
- **Adjustment Factors**: - **Adjustment Factors**:
- Accumulated shortfalls from previous extractions - Accumulated redistribution adjustments from previous extractions
- Committed UNDER/OVER payouts - Committed UNDER/OVER payouts
- Total intake from all betting activity - Total intake from all betting activity
...@@ -228,7 +230,7 @@ Send `MATCH_DONE` message to advance to next match. ...@@ -228,7 +230,7 @@ Send `MATCH_DONE` message to advance to next match.
### Profit Maximization ### Profit Maximization
- **Weighted Selection**: Prefers results maximizing redistribution - **Weighted Selection**: Prefers results maximizing redistribution
- **Fallback Protection**: Always selects result, even if CAP exceeded - **Fallback Protection**: Always selects result, even if CAP exceeded
- **Shortfall Carryover**: Tracks and compensates for under-redistribution - **Adjustment Carryover**: Tracks and compensates for redistribution imbalances (both under and over)
### Data Integrity ### Data Integrity
- **Transaction Safety**: All updates in database transactions - **Transaction Safety**: All updates in database transactions
...@@ -238,7 +240,7 @@ Send `MATCH_DONE` message to advance to next match. ...@@ -238,7 +240,7 @@ Send `MATCH_DONE` message to advance to next match.
## Configuration Parameters ## Configuration Parameters
- **CAP Percentage**: Default 70%, configurable via `extraction_redistribution_cap` - **CAP Percentage**: Default 70%, configurable via `extraction_redistribution_cap`
- **Shortfall Tracking**: Daily accumulated shortfalls affect future CAP calculations - **Adjustment Tracking**: Daily accumulated redistribution adjustments affect future CAP calculations
- **Result Associations**: Configurable via `extraction_associations` table - **Result Associations**: Configurable via `extraction_associations` table
- **Betting Mode**: Affects match status progression but not extraction logic - **Betting Mode**: Affects match status progression but not extraction logic
......
...@@ -12,7 +12,7 @@ from typing import Optional, Dict, Any, List ...@@ -12,7 +12,7 @@ from typing import Optional, Dict, Any, List
from .thread_manager import ThreadedComponent from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..database.manager import DatabaseManager from ..database.manager import DatabaseManager
from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel, ExtractionAssociationModel, DailyRedistributionShortfallModel, MatchTemplateModel, MatchOutcomeTemplateModel from ..database.models import MatchModel, MatchStatus, BetDetailModel, MatchOutcomeModel, GameConfigModel, ExtractionAssociationModel, PersistentRedistributionAdjustmentModel, MatchTemplateModel, MatchOutcomeTemplateModel
from ..utils.timezone_utils import get_today_venue_date from ..utils.timezone_utils import get_today_venue_date
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -2046,60 +2046,65 @@ class GamesThread(ThreadedComponent): ...@@ -2046,60 +2046,65 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to get redistribution CAP: {e}") logger.error(f"Failed to get redistribution CAP: {e}")
return 70.0 return 70.0
def _get_daily_shortfall(self, date, session) -> float: def _get_daily_redistribution_adjustment(self, date, session) -> float:
"""Get accumulated shortfall for a specific date""" """Get accumulated redistribution adjustment (persistent across application restarts)"""
try: try:
shortfall_record = session.query(DailyRedistributionShortfallModel).filter_by( # Get the most recent accumulated_shortfall value from any record
date=date latest_record = session.query(PersistentRedistributionAdjustmentModel).order_by(
PersistentRedistributionAdjustmentModel.updated_at.desc()
).first() ).first()
if shortfall_record: if latest_record:
logger.debug(f"Found shortfall record for {date}: {shortfall_record.accumulated_shortfall}") logger.debug(f"Found persistent redistribution adjustment: {latest_record.accumulated_shortfall}")
return shortfall_record.accumulated_shortfall return latest_record.accumulated_shortfall
else: else:
logger.debug(f"No shortfall record found for {date}, returning 0.0") logger.debug("No redistribution adjustment records found, returning 0.0")
return 0.0 return 0.0
except Exception as e: except Exception as e:
logger.error(f"Failed to get daily shortfall for {date}: {e}") logger.error(f"Failed to get persistent redistribution adjustment: {e}")
return 0.0 return 0.0
def _update_daily_shortfall(self, date, payin_amount, redistributed_amount, cap_percentage, session): def _update_daily_redistribution_adjustment(self, date, payin_amount, redistributed_amount, cap_percentage, session):
"""Update daily shortfall tracking after extraction""" """Update persistent redistribution adjustment tracking after extraction"""
try: try:
# Calculate the shortfall for this extraction # Calculate the redistribution adjustment for this extraction
# Positive: under-redistribution (shortfall), Negative: over-redistribution (surplus)
expected_redistribution = payin_amount * (cap_percentage / 100.0) expected_redistribution = payin_amount * (cap_percentage / 100.0)
shortfall = max(0, expected_redistribution - redistributed_amount) adjustment = expected_redistribution - redistributed_amount
logger.info(f"💰 [SHORTFALL DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Shortfall: {shortfall:.2f}") logger.info(f"💰 [ADJUSTMENT DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Adjustment: {adjustment:.2f}")
# Get or create the daily record # Use a fixed date for the global persistent record
shortfall_record = session.query(DailyRedistributionShortfallModel).filter_by( global_date = datetime.date(1970, 1, 1) # Fixed date for global record
date=date
# Get or create the global record
adjustment_record = session.query(PersistentRedistributionAdjustmentModel).filter_by(
date=global_date
).first() ).first()
if not shortfall_record: if not adjustment_record:
shortfall_record = DailyRedistributionShortfallModel( adjustment_record = PersistentRedistributionAdjustmentModel(
date=date, date=global_date,
accumulated_shortfall=shortfall, accumulated_shortfall=adjustment,
total_payin=payin_amount, total_payin=payin_amount,
total_redistributed=redistributed_amount, total_redistributed=redistributed_amount,
cap_percentage=cap_percentage cap_percentage=cap_percentage
) )
session.add(shortfall_record) session.add(adjustment_record)
logger.info(f"Created new shortfall record for {date} with shortfall {shortfall:.2f}") logger.info(f"Created global redistribution adjustment record with adjustment {adjustment:.2f}")
else: else:
# Update existing record # Update existing global record
shortfall_record.accumulated_shortfall += shortfall adjustment_record.accumulated_shortfall += adjustment
shortfall_record.total_payin += payin_amount adjustment_record.total_payin += payin_amount
shortfall_record.total_redistributed += redistributed_amount adjustment_record.total_redistributed += redistributed_amount
shortfall_record.cap_percentage = cap_percentage # Update to latest adjustment_record.cap_percentage = cap_percentage # Update to latest
logger.info(f"Updated shortfall record for {date}, new accumulated shortfall: {shortfall_record.accumulated_shortfall:.2f}") logger.info(f"Updated global redistribution adjustment record, new accumulated adjustment: {adjustment_record.accumulated_shortfall:.2f}")
session.commit() session.commit()
except Exception as e: except Exception as e:
logger.error(f"Failed to update daily shortfall for {date}: {e}") logger.error(f"Failed to update persistent redistribution adjustment: {e}")
session.rollback() session.rollback()
def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str: def _weighted_random_selection(self, under_coeff: float, over_coeff: float) -> str:
...@@ -2580,10 +2585,10 @@ class GamesThread(ThreadedComponent): ...@@ -2580,10 +2585,10 @@ class GamesThread(ThreadedComponent):
# Get redistribution CAP # Get redistribution CAP
cap_percentage = self._get_redistribution_cap() cap_percentage = self._get_redistribution_cap()
# Get accumulated shortfall from previous extractions for today # Get accumulated redistribution adjustment from previous extractions for today
today = self._get_today_venue_date() today = self._get_today_venue_date()
accumulated_shortfall = self._get_daily_shortfall(today, session) accumulated_shortfall = self._get_daily_redistribution_adjustment(today, session)
logger.info(f"🎯 [EXTRACTION DEBUG] Accumulated shortfall: {accumulated_shortfall:.2f}") logger.info(f"🎯 [EXTRACTION DEBUG] Accumulated redistribution adjustment: {accumulated_shortfall:.2f}")
# Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets) # Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets)
total_payin_all_bets = total_payin + total_bet_amount total_payin_all_bets = total_payin + total_bet_amount
...@@ -2660,10 +2665,10 @@ class GamesThread(ThreadedComponent): ...@@ -2660,10 +2665,10 @@ class GamesThread(ThreadedComponent):
logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics") logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics")
self._collect_match_statistics(match_id, fixture_id, selected_result, session) self._collect_match_statistics(match_id, fixture_id, selected_result, session)
# Step 10: Update daily shortfall tracking # Step 10: Update daily redistribution adjustment tracking
logger.info(f"💰 [EXTRACTION DEBUG] Step 10: Updating daily shortfall tracking") logger.info(f"💰 [EXTRACTION DEBUG] Step 10: Updating daily redistribution adjustment tracking")
today = self._get_today_venue_date() today = self._get_today_venue_date()
self._update_daily_shortfall(today, total_payin_all_bets, payouts[selected_result], cap_percentage, session) self._update_daily_redistribution_adjustment(today, total_payin_all_bets, payouts[selected_result], cap_percentage, session)
logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}") logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}")
......
...@@ -2731,6 +2731,165 @@ class Migration_035_AddDailyRedistributionShortfallTable(DatabaseMigration): ...@@ -2731,6 +2731,165 @@ class Migration_035_AddDailyRedistributionShortfallTable(DatabaseMigration):
return False return False
class Migration_037_RenameDailyRedistributionShortfallTable(DatabaseMigration):
"""Rename daily_redistribution_shortfall table to persistent_redistribution_adjustment"""
def __init__(self):
super().__init__("037", "Rename daily_redistribution_shortfall table to persistent_redistribution_adjustment")
def up(self, db_manager) -> bool:
"""Rename the table"""
try:
with db_manager.engine.connect() as conn:
# Check if the old table exists
result = conn.execute(text("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='daily_redistribution_shortfall'
"""))
old_table_exists = result.fetchone() is not None
if old_table_exists:
# Rename the table
conn.execute(text("""
ALTER TABLE daily_redistribution_shortfall
RENAME TO persistent_redistribution_adjustment
"""))
# Note: SQLite doesn't support ALTER INDEX RENAME, so we'll leave the old index name
# The index will be recreated with the new name when the table is recreated in future migrations
conn.commit()
logger.info("Renamed daily_redistribution_shortfall table to persistent_redistribution_adjustment")
else:
logger.info("daily_redistribution_shortfall table does not exist, nothing to rename")
return True
except Exception as e:
logger.error(f"Failed to rename daily_redistribution_shortfall table: {e}")
return False
def down(self, db_manager) -> bool:
"""Rename back to the old table name"""
try:
with db_manager.engine.connect() as conn:
# Check if the new table exists
result = conn.execute(text("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='persistent_redistribution_adjustment'
"""))
new_table_exists = result.fetchone() is not None
if new_table_exists:
# Rename back to the old name
conn.execute(text("""
ALTER TABLE persistent_redistribution_adjustment
RENAME TO daily_redistribution_shortfall
"""))
# Note: SQLite doesn't support ALTER INDEX RENAME, so we'll leave the index name as-is
conn.commit()
logger.info("Renamed persistent_redistribution_adjustment table back to daily_redistribution_shortfall")
else:
logger.info("persistent_redistribution_adjustment table does not exist, nothing to rename back")
return True
except Exception as e:
logger.error(f"Failed to rename back to daily_redistribution_shortfall table: {e}")
return False
class Migration_038_AddWin1Win2Associations(DatabaseMigration):
"""Add WIN1 and WIN2 result options and extraction associations for X1, X2, and 12 outcomes"""
def __init__(self):
super().__init__("038", "Add WIN1 and WIN2 result options and extraction associations for X1, X2, and 12 outcomes")
def up(self, db_manager) -> bool:
"""Add WIN1 and WIN2 result options and associations if they don't exist"""
try:
with db_manager.engine.connect() as conn:
# Add WIN1 and WIN2 to result_options table
result_options = [
('WIN1', 'Fighter 1 wins result', 9),
('WIN2', 'Fighter 2 wins result', 10)
]
for result_name, description, sort_order in result_options:
conn.execute(text("""
INSERT OR IGNORE INTO result_options
(result_name, description, is_active, sort_order, created_at, updated_at)
VALUES (:result_name, :description, 1, :sort_order, datetime('now'), datetime('now'))
"""), {
'result_name': result_name,
'description': description,
'sort_order': sort_order
})
# Add WIN1 associations: X1 and 12 -> WIN1
associations = [
('X1', 'WIN1'),
('12', 'WIN1'),
('X2', 'WIN2'),
('12', 'WIN2')
]
for outcome_name, extraction_result in associations:
conn.execute(text("""
INSERT OR IGNORE INTO extraction_associations
(outcome_name, extraction_result, is_default, created_at, updated_at)
VALUES (:outcome_name, :extraction_result, 1, datetime('now'), datetime('now'))
"""), {
'outcome_name': outcome_name,
'extraction_result': extraction_result
})
conn.commit()
logger.info("Added WIN1 and WIN2 result options and extraction associations")
return True
except Exception as e:
logger.error(f"Failed to add WIN1 and WIN2 result options and associations: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove the WIN1 and WIN2 result options and associations added by this migration"""
try:
with db_manager.engine.connect() as conn:
# Remove WIN1 and WIN2 from result_options
conn.execute(text("""
DELETE FROM result_options
WHERE result_name IN ('WIN1', 'WIN2')
"""))
# Remove the specific associations added by this migration
associations = [
('X1', 'WIN1'),
('12', 'WIN1'),
('X2', 'WIN2'),
('12', 'WIN2')
]
for outcome_name, extraction_result in associations:
conn.execute(text("""
DELETE FROM extraction_associations
WHERE outcome_name = :outcome_name
AND extraction_result = :extraction_result
AND is_default = 1
"""), {
'outcome_name': outcome_name,
'extraction_result': extraction_result
})
conn.commit()
logger.info("Removed WIN1 and WIN2 result options and extraction associations")
return True
except Exception as e:
logger.error(f"Failed to remove WIN1 and WIN2 result options and associations: {e}")
return False
class Migration_036_AddMatchTemplatesTables(DatabaseMigration): class Migration_036_AddMatchTemplatesTables(DatabaseMigration):
"""Add matches_templates and match_outcomes_templates tables for storing match templates""" """Add matches_templates and match_outcomes_templates tables for storing match templates"""
...@@ -2886,6 +3045,8 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -2886,6 +3045,8 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_034_AddDefaultLicenseText(), Migration_034_AddDefaultLicenseText(),
Migration_035_AddDailyRedistributionShortfallTable(), Migration_035_AddDailyRedistributionShortfallTable(),
Migration_036_AddMatchTemplatesTables(), Migration_036_AddMatchTemplatesTables(),
Migration_037_RenameDailyRedistributionShortfallTable(),
Migration_038_AddWin1Win2Associations(),
] ]
......
...@@ -947,22 +947,22 @@ class ExtractionStatsModel(BaseModel): ...@@ -947,22 +947,22 @@ class ExtractionStatsModel(BaseModel):
return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>' return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>'
class DailyRedistributionShortfallModel(BaseModel): class PersistentRedistributionAdjustmentModel(BaseModel):
"""Daily redistribution shortfall tracking for CAP adjustments""" """Persistent redistribution adjustment tracking across application restarts"""
__tablename__ = 'daily_redistribution_shortfall' __tablename__ = 'persistent_redistribution_adjustments'
__table_args__ = ( __table_args__ = (
Index('ix_daily_redistribution_shortfall_date', 'date'), Index('ix_persistent_redistribution_adjustments_date', 'date'),
UniqueConstraint('date', name='uq_daily_redistribution_shortfall_date'), UniqueConstraint('date', name='uq_persistent_redistribution_adjustments_date'),
) )
date = Column(Date, nullable=False, unique=True, comment='Date for shortfall tracking') date = Column(Date, nullable=False, unique=True, comment='Date for adjustment tracking')
accumulated_shortfall = Column(Float(precision=2), default=0.0, nullable=False, comment='Accumulated shortfall from previous extractions') accumulated_shortfall = Column(Float(precision=2), default=0.0, nullable=False, comment='Accumulated adjustment from previous extractions')
total_payin = Column(Float(precision=2), default=0.0, nullable=False, comment='Total payin for the day') total_payin = Column(Float(precision=2), default=0.0, nullable=False, comment='Total payin for the day')
total_redistributed = Column(Float(precision=2), default=0.0, nullable=False, comment='Total redistributed for the day') total_redistributed = Column(Float(precision=2), default=0.0, nullable=False, comment='Total redistributed for the day')
cap_percentage = Column(Float(precision=2), default=70.0, nullable=False, comment='CAP percentage used for calculations') cap_percentage = Column(Float(precision=2), default=70.0, nullable=False, comment='CAP percentage used for calculations')
def __repr__(self): def __repr__(self):
return f'<DailyRedistributionShortfall {self.date}: shortfall={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>' return f'<PersistentRedistributionAdjustment {self.date}: adjustment={self.accumulated_shortfall:.2f}, payin={self.total_payin:.2f}, redistributed={self.total_redistributed:.2f}>'
class MatchTemplateModel(BaseModel): class MatchTemplateModel(BaseModel):
......
...@@ -4108,6 +4108,100 @@ def save_redistribution_cap(): ...@@ -4108,6 +4108,100 @@ def save_redistribution_cap():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Redistribution Balance API routes (admin-only)
@api_bp.route('/redistribution-balance')
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def get_redistribution_balance():
"""Get current redistribution balance (admin only)"""
try:
from ..database.models import PersistentRedistributionAdjustmentModel
from datetime import date
session = api_bp.db_manager.get_session()
try:
# Get the latest redistribution adjustment record
latest_record = session.query(PersistentRedistributionAdjustmentModel)\
.order_by(PersistentRedistributionAdjustmentModel.date.desc())\
.first()
current_balance = 0.0
if latest_record:
current_balance = float(latest_record.accumulated_shortfall)
return jsonify({
"success": True,
"redistribution_balance": current_balance,
"last_updated": latest_record.date.isoformat() if latest_record else None
})
finally:
session.close()
except Exception as e:
logger.error(f"API get redistribution balance error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/redistribution-balance/reset', methods=['POST'])
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def reset_redistribution_balance():
"""Reset redistribution balance to zero (admin only)"""
try:
from ..database.models import PersistentRedistributionAdjustmentModel
from datetime import date, datetime
session = api_bp.db_manager.get_session()
try:
# Get the latest redistribution adjustment record
latest_record = session.query(PersistentRedistributionAdjustmentModel)\
.order_by(PersistentRedistributionAdjustmentModel.date.desc())\
.first()
if latest_record:
# Reset the accumulated shortfall to zero
old_balance = float(latest_record.accumulated_shortfall)
latest_record.accumulated_shortfall = 0.0
latest_record.updated_at = datetime.utcnow()
session.commit()
logger.info(f"Redistribution balance reset from {old_balance} to 0.0")
return jsonify({
"success": True,
"message": f"Redistribution balance reset from {old_balance:.2f} to 0.00",
"old_balance": old_balance,
"new_balance": 0.0
})
else:
# No record exists, create one with zero balance
today = date.today()
new_record = PersistentRedistributionAdjustmentModel(
date=today,
accumulated_shortfall=0.0,
cap_percentage=70.0 # Default cap
)
session.add(new_record)
session.commit()
logger.info("Created new redistribution adjustment record with zero balance")
return jsonify({
"success": True,
"message": "Redistribution balance set to 0.00 (new record created)",
"old_balance": None,
"new_balance": 0.0
})
finally:
session.close()
except Exception as e:
logger.error(f"API reset redistribution balance error: {e}")
return jsonify({"error": str(e)}), 500
# Currency Settings API routes # Currency Settings API routes
@api_bp.route('/currency-settings') @api_bp.route('/currency-settings')
@get_api_auth_decorator() @get_api_auth_decorator()
......
...@@ -122,6 +122,42 @@ ...@@ -122,6 +122,42 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
<!-- Redistribution Balance Management (Admin Only) -->
{% if current_user.is_admin %}
<div class="row mt-3 pt-3 border-top">
<div class="col-12">
<h6 class="text-muted mb-3">
<i class="fas fa-balance-scale me-2"></i>Redistribution Balance
</h6>
</div>
<div class="col-md-6 mb-3">
<div class="card bg-light">
<div class="card-body text-center p-3">
<h5 class="card-title mb-2">
<i class="fas fa-coins me-2"></i>Current Balance
</h5>
<div class="h4 mb-2 text-primary" id="redistribution-balance">--</div>
<div class="mt-2">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>Balance to compensate over/under redistribution
</small>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="d-grid">
<button class="btn btn-outline-danger w-100" id="btn-reset-redistribution-balance">
<i class="fas fa-undo me-2"></i>Reset Balance to Zero
</button>
<small class="text-muted mt-2 d-block">
<i class="fas fa-exclamation-triangle me-1"></i>This will reset the redistribution compensation balance
</small>
</div>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
...@@ -459,6 +495,11 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -459,6 +495,11 @@ document.addEventListener('DOMContentLoaded', function() {
// Load license text configuration // Load license text configuration
loadLicenseText(); loadLicenseText();
// Load redistribution balance (admin only)
if (document.getElementById('redistribution-balance')) {
loadRedistributionBalance();
}
// Quick action buttons // Quick action buttons
document.getElementById('btn-play-video').addEventListener('click', function() { document.getElementById('btn-play-video').addEventListener('click', function() {
...@@ -499,6 +540,14 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -499,6 +540,14 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-reset-license').addEventListener('click', function() { document.getElementById('btn-reset-license').addEventListener('click', function() {
resetLicenseText(); resetLicenseText();
}); });
// Redistribution balance reset button (only exists for admin)
const resetBalanceBtn = document.getElementById('btn-reset-redistribution-balance');
if (resetBalanceBtn) {
resetBalanceBtn.addEventListener('click', function() {
resetRedistributionBalance();
});
}
// Admin shutdown button (only exists if user is admin) // Admin shutdown button (only exists if user is admin)
const shutdownBtn = document.getElementById('btn-shutdown-app'); const shutdownBtn = document.getElementById('btn-shutdown-app');
...@@ -1012,5 +1061,70 @@ function resetLicenseText() { ...@@ -1012,5 +1061,70 @@ function resetLicenseText() {
document.getElementById('license-text').value = defaultText; document.getElementById('license-text').value = defaultText;
} }
function loadRedistributionBalance() {
fetch('/api/redistribution-balance')
.then(response => response.json())
.then(data => {
if (data.success) {
const balanceElement = document.getElementById('redistribution-balance');
const balance = data.redistribution_balance;
balanceElement.textContent = balance.toFixed(2);
// Color coding: green for positive, red for negative, neutral for zero
if (balance > 0) {
balanceElement.className = 'h4 mb-2 text-success';
} else if (balance < 0) {
balanceElement.className = 'h4 mb-2 text-danger';
} else {
balanceElement.className = 'h4 mb-2 text-primary';
}
} else {
document.getElementById('redistribution-balance').textContent = 'Error';
console.error('Failed to load redistribution balance:', data.error);
}
})
.catch(error => {
document.getElementById('redistribution-balance').textContent = 'Error';
console.error('Error loading redistribution balance:', error);
});
}
function resetRedistributionBalance() {
if (!confirm('Are you sure you want to reset the redistribution balance to zero? This action cannot be undone.')) {
return;
}
const resetBtn = document.getElementById('btn-reset-redistribution-balance');
const originalText = resetBtn.innerHTML;
// Show loading state
resetBtn.disabled = true;
resetBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Resetting...';
fetch('/api/redistribution-balance/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
// Reload the balance
loadRedistributionBalance();
} else {
alert('Failed to reset redistribution balance: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error resetting redistribution balance: ' + error.message);
})
.finally(() => {
// Restore button state
resetBtn.disabled = false;
resetBtn.innerHTML = originalText;
});
}
</script> </script>
{% endblock %} {% endblock %}
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