feat: enhance extraction page with available bets and result associations

- Rename 'Available Outcomes' to 'Available Bets' in admin interface
- Add database tables for available_bets and result_options
- Implement CRUD operations for managing bets and result options
- Update extraction page UI with add/delete functionality
- Implement drag-and-drop associations between bets and results
- Add database persistence for all associations
- Remove X buttons from result association columns for cleaner UI
- Update terminology throughout for clarity
parent 9fd38518
...@@ -11,7 +11,8 @@ from .models import ( ...@@ -11,7 +11,8 @@ from .models import (
ApiTokenModel, ApiTokenModel,
LogEntryModel, LogEntryModel,
TemplateModel, TemplateModel,
AvailableBetModel AvailableBetModel,
ResultOptionModel
) )
from .migrations import DatabaseMigration, run_migrations from .migrations import DatabaseMigration, run_migrations
...@@ -25,6 +26,7 @@ __all__ = [ ...@@ -25,6 +26,7 @@ __all__ = [
'LogEntryModel', 'LogEntryModel',
'TemplateModel', 'TemplateModel',
'AvailableBetModel', 'AvailableBetModel',
'ResultOptionModel',
'DatabaseMigration', 'DatabaseMigration',
'run_migrations' 'run_migrations'
] ]
\ No newline at end of file
...@@ -1807,6 +1807,107 @@ class Migration_023_AddAvailableBetsTable(DatabaseMigration): ...@@ -1807,6 +1807,107 @@ class Migration_023_AddAvailableBetsTable(DatabaseMigration):
logger.error(f"Failed to drop available_bets table: {e}") logger.error(f"Failed to drop available_bets table: {e}")
return False return False
class Migration_024_AddResultOptionsTable(DatabaseMigration):
"""Add result_options table for managing result options in extraction page"""
def __init__(self):
super().__init__("024", "Add result_options table for managing result options")
def up(self, db_manager) -> bool:
"""Create result_options table with default result options"""
try:
with db_manager.engine.connect() as conn:
# Create result_options table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS result_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
result_name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE NOT NULL,
sort_order INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
# Create indexes for result_options table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_result_options_result_name ON result_options(result_name)",
"CREATE INDEX IF NOT EXISTS ix_result_options_is_active ON result_options(is_active)",
"CREATE INDEX IF NOT EXISTS ix_result_options_sort_order ON result_options(sort_order)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
# Insert default result options as specified by user
default_results = [
('DRAW', 'Draw result', 1),
('DKO', 'Double Knockout result', 2),
('WIN1', 'Fighter 1 wins result', 3),
('WIN2', 'Fighter 2 wins result', 4),
('RET1', 'Fighter 1 retires result', 5),
('RET2', 'Fighter 2 retires result', 6),
('PTS1', 'Fighter 1 wins by points result', 7),
('PTS2', 'Fighter 2 wins by points result', 8),
('KO1', 'Fighter 1 wins by KO result', 9),
('KO2', 'Fighter 2 wins by KO result', 10),
]
for result_name, description, sort_order in default_results:
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
})
conn.commit()
logger.info("Result options table created with default result options")
return True
except Exception as e:
logger.error(f"Failed to create result_options table: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop result_options table"""
try:
with db_manager.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS result_options"))
conn.commit()
logger.info("Result options table dropped")
return True
except Exception as e:
logger.error(f"Failed to drop result_options table: {e}")
return False
class Migration_025_AddResultOptionModel(DatabaseMigration):
"""Add ResultOptionModel to models.py for result options management"""
def __init__(self):
super().__init__("025", "Add ResultOptionModel to models.py for result options management")
def up(self, db_manager) -> bool:
"""This migration just ensures the ResultOptionModel is available - no database changes needed"""
try:
logger.info("ResultOptionModel migration - no database changes required")
return True
except Exception as e:
logger.error(f"Failed to apply ResultOptionModel migration: {e}")
return False
def down(self, db_manager) -> bool:
"""No database changes to rollback"""
logger.info("ResultOptionModel migration rollback - no database changes")
return True
# Registry of all migrations in order # Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [ MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(), Migration_001_InitialSchema(),
...@@ -1832,6 +1933,8 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -1832,6 +1933,8 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_021_AddBarcodeConfiguration(), Migration_021_AddBarcodeConfiguration(),
Migration_022_AddQRCodeConfiguration(), Migration_022_AddQRCodeConfiguration(),
Migration_023_AddAvailableBetsTable(), Migration_023_AddAvailableBetsTable(),
Migration_024_AddResultOptionsTable(),
Migration_025_AddResultOptionModel(),
] ]
......
...@@ -781,3 +781,22 @@ class AvailableBetModel(BaseModel): ...@@ -781,3 +781,22 @@ class AvailableBetModel(BaseModel):
def __repr__(self): def __repr__(self):
return f'<AvailableBet {self.bet_name}: {self.description}>' return f'<AvailableBet {self.bet_name}: {self.description}>'
class ResultOptionModel(BaseModel):
"""Result options for extraction system results area"""
__tablename__ = 'result_options'
__table_args__ = (
Index('ix_result_options_result_name', 'result_name'),
Index('ix_result_options_is_active', 'is_active'),
Index('ix_result_options_sort_order', 'sort_order'),
UniqueConstraint('result_name', name='uq_result_options_result_name'),
)
result_name = Column(String(50), nullable=False, unique=True, comment='Result option name (e.g., DRAW, WIN1, KO1)')
description = Column(String(255), comment='Description of the result option')
is_active = Column(Boolean, default=True, nullable=False, comment='Whether this result option is active')
sort_order = Column(Integer, default=0, comment='Sort order for display')
def __repr__(self):
return f'<ResultOption {self.result_name}: {self.description}>'
...@@ -3531,6 +3531,216 @@ def delete_available_bet(): ...@@ -3531,6 +3531,216 @@ def delete_available_bet():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Result Options API routes
@api_bp.route('/extraction/result-options')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_result_options():
"""Get all result options for extraction"""
try:
from ..database.models import ResultOptionModel
session = api_bp.db_manager.get_session()
try:
# Get all result options
result_options = session.query(ResultOptionModel).order_by(ResultOptionModel.result_name.asc()).all()
options_data = [option.to_dict() for option in result_options]
return jsonify({
"success": True,
"options": options_data,
"total": len(options_data)
})
finally:
session.close()
except Exception as e:
logger.error(f"API get result options error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/extraction/result-options/add', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def add_result_option():
"""Add a new result option"""
try:
from ..database.models import ResultOptionModel
data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper()
description = data.get('description', '').strip()
if not result_name:
return jsonify({"error": "result_name is required"}), 400
if len(result_name) > 50:
return jsonify({"error": "result_name must be 50 characters or less"}), 400
session = api_bp.db_manager.get_session()
try:
# Check if result option already exists
existing_option = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if existing_option:
return jsonify({"error": f"Result option '{result_name}' already exists"}), 400
# Get the highest sort order
max_sort = session.query(ResultOptionModel.sort_order).order_by(ResultOptionModel.sort_order.desc()).first()
sort_order = (max_sort[0] + 1) if max_sort and max_sort[0] else 1
# Create new result option
new_option = ResultOptionModel(
result_name=result_name,
description=description or f"Result option: {result_name}",
sort_order=sort_order
)
session.add(new_option)
session.commit()
logger.info(f"Added result option: {result_name}")
return jsonify({
"success": True,
"message": f"Result option '{result_name}' added successfully",
"option": new_option.to_dict()
})
finally:
session.close()
except Exception as e:
logger.error(f"API add result option error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/extraction/result-options/delete', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def delete_result_option():
"""Delete a result option"""
try:
from ..database.models import ResultOptionModel
data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper()
if not result_name:
return jsonify({"error": "result_name is required"}), 400
session = api_bp.db_manager.get_session()
try:
# Find the result option
option = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if not option:
return jsonify({"error": f"Result option '{result_name}' not found"}), 404
# Delete the result option
session.delete(option)
session.commit()
logger.info(f"Deleted result option: {result_name}")
return jsonify({
"success": True,
"message": f"Result option '{result_name}' deleted successfully"
})
finally:
session.close()
except Exception as e:
logger.error(f"API delete result option error: {e}")
return jsonify({"error": str(e)}), 500
# Migration to create result_options table with default values
def create_result_options_migration():
"""Create migration for result_options table"""
try:
from ..database.models import ResultOptionModel
from ..database.migrations import DatabaseMigration
class Migration_023(DatabaseMigration):
version = "023"
description = "Add result_options table for extraction system results area"
def up(self, db_manager):
"""Apply migration"""
try:
session = db_manager.get_session()
try:
# Create table if it doesn't exist
ResultOptionModel.__table__.create(session.bind, checkfirst=True)
# Insert default result options
default_options = [
('DRAW', 'Draw result', 1),
('DKO', 'Double Knockout', 2),
('WIN1', 'Fighter 1 wins', 3),
('WIN2', 'Fighter 2 wins', 4),
('RET1', 'Fighter 1 retires', 5),
('RET2', 'Fighter 2 retires', 6),
('PTS1', 'Fighter 1 wins by points', 7),
('PTS2', 'Fighter 2 wins by points', 8),
('KO1', 'Fighter 1 wins by KO', 9),
('KO2', 'Fighter 2 wins by KO', 10)
]
for result_name, description, sort_order in default_options:
# Check if already exists
existing = session.query(ResultOptionModel).filter_by(result_name=result_name).first()
if not existing:
option = ResultOptionModel(
result_name=result_name,
description=description,
sort_order=sort_order
)
session.add(option)
session.commit()
logger.info("Created result_options table with default values")
return True
finally:
session.close()
except Exception as e:
logger.error(f"Migration 023 failed: {e}")
return False
def down(self, db_manager):
"""Rollback migration"""
try:
session = db_manager.get_session()
try:
# Drop table
ResultOptionModel.__table__.drop(session.bind, checkfirst=True)
session.commit()
logger.info("Dropped result_options table")
return True
finally:
session.close()
except Exception as e:
logger.error(f"Migration 023 rollback failed: {e}")
return False
return Migration_023()
except Exception as e:
logger.error(f"Failed to create result options migration: {e}")
return None
# Add the migration to the migrations list
try:
from ..database.migrations import MIGRATIONS
migration_023 = create_result_options_migration()
if migration_023:
MIGRATIONS.append(migration_023)
logger.info("Added Migration 023 for result_options table")
except Exception as e:
logger.error(f"Failed to add Migration 023: {e}")
# Redistribution CAP API routes (admin-only) # Redistribution CAP API routes (admin-only)
@api_bp.route('/extraction/redistribution-cap') @api_bp.route('/extraction/redistribution-cap')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
......
...@@ -144,11 +144,21 @@ ...@@ -144,11 +144,21 @@
<!-- Results Area --> <!-- Results Area -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0"> <div>
<i class="fas fa-target me-2"></i>Results <h5 class="card-title mb-0">
</h5> <i class="fas fa-target me-2"></i>Results
<small class="text-muted">Drop outcomes here to create association columns</small> </h5>
<small class="text-muted">Drop outcomes here to create association columns</small>
</div>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-success btn-sm" id="btn-add-result-option">
<i class="fas fa-plus me-1"></i>Add
</button>
<button type="button" class="btn btn-outline-danger btn-sm" id="btn-delete-result-option">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="results-pool" class="results-pool"> <div id="results-pool" class="results-pool">
...@@ -167,9 +177,9 @@ ...@@ -167,9 +177,9 @@
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<div> <div>
<h5 class="card-title mb-0"> <h5 class="card-title mb-0">
<i class="fas fa-columns me-2"></i>Outcome Associations <i class="fas fa-columns me-2"></i>Result Associations
</h5> </h5>
<small class="text-muted">Each outcome gets its own column for associations</small> <small class="text-muted">Each result option from the Results area gets its own column for bet associations</small>
</div> </div>
<button id="btn-clear-all" class="btn btn-outline-danger btn-sm"> <button id="btn-clear-all" class="btn btn-outline-danger btn-sm">
<i class="fas fa-trash me-1"></i>Clear All <i class="fas fa-trash me-1"></i>Clear All
...@@ -248,6 +258,57 @@ ...@@ -248,6 +258,57 @@
</div> </div>
</div> </div>
<!-- Add Result Option Modal -->
<div class="modal fade" id="addResultOptionModal" tabindex="-1" aria-labelledby="addResultOptionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addResultOptionModalLabel">Add Result Option</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="add-result-option-form">
<div class="mb-3">
<label for="result-option-name" class="form-label">Result Option Name</label>
<input type="text" class="form-control" id="result-option-name" placeholder="e.g., DRAW, DKO, WIN1" required maxlength="50">
<div class="form-text">Enter the result option name (max 50 characters)</div>
</div>
<div class="mb-3">
<label for="result-option-description" class="form-label">Description (Optional)</label>
<input type="text" class="form-control" id="result-option-description" placeholder="Description of the result option" maxlength="255">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-save-result-option">Add Result Option</button>
</div>
</div>
</div>
</div>
<!-- Delete Result Option Modal -->
<div class="modal fade" id="deleteResultOptionModal" tabindex="-1" aria-labelledby="deleteResultOptionModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteResultOptionModalLabel">Delete Result Option</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Select a result option to delete:</p>
<select class="form-select" id="delete-result-option-select" size="5">
<option value="">Loading result options...</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="btn-confirm-delete-result-option">Delete</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
...@@ -331,12 +392,6 @@ ...@@ -331,12 +392,6 @@
border-radius: 0.5rem; border-radius: 0.5rem;
padding: 1rem; padding: 1rem;
background: #f8f9fa; background: #f8f9fa;
transition: all 0.3s ease;
}
.results-pool.drag-over {
border-color: #007bff;
background: #e7f1ff;
} }
.results-pool .badge { .results-pool .badge {
...@@ -564,6 +619,8 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -564,6 +619,8 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('btn-save-time-limit').addEventListener('click', saveTimeLimitConfig); document.getElementById('btn-save-time-limit').addEventListener('click', saveTimeLimitConfig);
document.getElementById('btn-clear-all').addEventListener('click', clearAllAssociations); document.getElementById('btn-clear-all').addEventListener('click', clearAllAssociations);
document.getElementById('btn-add-bet').addEventListener('click', addNewBet); document.getElementById('btn-add-bet').addEventListener('click', addNewBet);
document.getElementById('btn-add-result-option').addEventListener('click', showAddResultOptionModal);
document.getElementById('btn-delete-result-option').addEventListener('click', showDeleteResultOptionModal);
// Redistribution CAP listeners (admin only) // Redistribution CAP listeners (admin only)
if (isAdmin) { if (isAdmin) {
...@@ -644,12 +701,12 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -644,12 +701,12 @@ document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('outcome-columns'); const container = document.getElementById('outcome-columns');
container.innerHTML = ''; container.innerHTML = '';
// Show columns for outcomes that are in Results area // Show columns for result options that are in Results area
if (resultsOutcomes.length === 0) { if (resultsOutcomes.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="text-center text-muted p-4"> <div class="text-center text-muted p-4">
<i class="fas fa-info-circle me-2"></i> <i class="fas fa-info-circle me-2"></i>
Drag outcomes to the "Results" area on the left to create association columns here Add result options to the "Results" area on the left to create association columns here
</div> </div>
`; `;
return; return;
...@@ -663,14 +720,11 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -663,14 +720,11 @@ document.addEventListener('DOMContentLoaded', function() {
column.innerHTML = ` column.innerHTML = `
<div class="outcome-column-header"> <div class="outcome-column-header">
${outcome} ${outcome}
<button type="button" class="btn btn-sm btn-outline-light ms-2"
onclick="removeFromResults('${outcome}')"
style="font-size: 0.7em; padding: 0.1rem 0.3rem;">×</button>
</div> </div>
<div class="outcome-column-body" data-drop-target="${outcome}"> <div class="outcome-column-body" data-drop-target="${outcome}">
<div class="empty-column-message"> <div class="empty-column-message">
<i class="fas fa-arrow-down me-2"></i> <i class="fas fa-arrow-down me-2"></i>
Drop outcomes here to associate with ${outcome} Drop bets here to associate with ${outcome}
</div> </div>
</div> </div>
`; `;
...@@ -851,22 +905,17 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -851,22 +905,17 @@ document.addEventListener('DOMContentLoaded', function() {
if (element.hasAttribute('data-drop-target')) { if (element.hasAttribute('data-drop-target')) {
return element.closest('.outcome-column'); return element.closest('.outcome-column');
} }
// Check for UNDER/OVER zones // Check for UNDER/OVER zones
if (element.classList.contains('under-over-drop') || element.closest('.under-over-zone')) { if (element.classList.contains('under-over-drop') || element.closest('.under-over-zone')) {
return element.closest('.under-over-zone'); return element.closest('.under-over-zone');
} }
// Check for Results pool
if (element.classList.contains('results-pool') || element.closest('.results-pool')) {
return element.classList.contains('results-pool') ? element : element.closest('.results-pool');
}
// Check for trash bin // Check for trash bin
if (element.classList.contains('trash-bin') || element.closest('.trash-bin')) { if (element.classList.contains('trash-bin') || element.closest('.trash-bin')) {
return element.classList.contains('trash-bin') ? element : element.closest('.trash-bin'); return element.classList.contains('trash-bin') ? element : element.closest('.trash-bin');
} }
return null; return null;
} }
...@@ -906,9 +955,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -906,9 +955,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Drop on UNDER/OVER zone - any outcome can be assigned // Drop on UNDER/OVER zone - any outcome can be assigned
const targetZone = target.dataset.outcome; // 'UNDER' or 'OVER' const targetZone = target.dataset.outcome; // 'UNDER' or 'OVER'
addToUnderOverZone(targetZone, draggedOutcome); addToUnderOverZone(targetZone, draggedOutcome);
} else if (target.classList.contains('results-pool')) {
// Drop on Results area - creates a new column
addToResults(draggedOutcome);
} else if (target.classList.contains('trash-bin')) { } else if (target.classList.contains('trash-bin')) {
// Drop on trash bin // Drop on trash bin
if (draggedElement.classList.contains('associated-outcome')) { if (draggedElement.classList.contains('associated-outcome')) {
...@@ -1020,13 +1066,13 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1020,13 +1066,13 @@ document.addEventListener('DOMContentLoaded', function() {
<span class="badge bg-primary position-relative"> <span class="badge bg-primary position-relative">
${outcome} ${outcome}
<button type="button" class="btn-close btn-close-white ms-1" <button type="button" class="btn-close btn-close-white ms-1"
onclick="removeFromResults('${outcome}')" onclick="deleteResultOption('${outcome}')"
style="font-size: 0.6em;"></button> style="font-size: 0.6em;"></button>
</span> </span>
`).join('')} `).join('')}
</div> </div>
<div class="text-center text-muted"> <div class="text-center text-muted">
<small><i class="fas fa-plus me-1"></i>Drop more outcomes to add columns</small> <small><i class="fas fa-plus me-1"></i>Use Add/Delete buttons to manage result options</small>
</div> </div>
`; `;
} }
...@@ -1207,17 +1253,47 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1207,17 +1253,47 @@ document.addEventListener('DOMContentLoaded', function() {
// Persistence functions for Results configuration // Persistence functions for Results configuration
function loadResultsConfig() { function loadResultsConfig() {
fetch('/api/extraction/results-config') // Load result options from database
fetch('/api/extraction/result-options')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
resultsOutcomes = data.results_outcomes || []; // Convert result options to the format expected by the UI
resultsOutcomes = data.options.map(option => option.result_name);
updateResultsPool(); updateResultsPool();
createOutcomeColumns(); createOutcomeColumns();
displayAssociations(); displayAssociations();
} else {
console.error('Failed to load result options:', data.error);
// Fallback to saved config if database fails
fetch('/api/extraction/results-config')
.then(response => response.json())
.then(fallbackData => {
if (fallbackData.success) {
resultsOutcomes = fallbackData.results_outcomes || [];
updateResultsPool();
createOutcomeColumns();
displayAssociations();
}
})
.catch(error => console.error('Error loading Results config fallback:', error));
} }
}) })
.catch(error => console.error('Error loading Results config:', error)); .catch(error => {
console.error('Error loading result options:', error);
// Fallback to saved config
fetch('/api/extraction/results-config')
.then(response => response.json())
.then(data => {
if (data.success) {
resultsOutcomes = data.results_outcomes || [];
updateResultsPool();
createOutcomeColumns();
displayAssociations();
}
})
.catch(error => console.error('Error loading Results config fallback:', error));
});
} }
function saveResultsConfig() { function saveResultsConfig() {
...@@ -1443,6 +1519,146 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1443,6 +1519,146 @@ document.addEventListener('DOMContentLoaded', function() {
alert('Error deleting bet'); alert('Error deleting bet');
}); });
}; };
// Result Options management functions
function showAddResultOptionModal() {
document.getElementById('add-result-option-form').reset();
const modal = new bootstrap.Modal(document.getElementById('addResultOptionModal'));
modal.show();
}
function showDeleteResultOptionModal() {
// Load result options for the select dropdown
fetch('/api/extraction/result-options')
.then(response => response.json())
.then(data => {
if (data.success) {
const select = document.getElementById('delete-result-option-select');
select.innerHTML = '';
if (data.options.length === 0) {
select.innerHTML = '<option value="">No result options available</option>';
document.getElementById('btn-confirm-delete-result-option').disabled = true;
} else {
data.options.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.result_name;
optionElement.textContent = `${option.result_name}${option.description ? ' - ' + option.description : ''}`;
select.appendChild(optionElement);
});
document.getElementById('btn-confirm-delete-result-option').disabled = false;
}
const modal = new bootstrap.Modal(document.getElementById('deleteResultOptionModal'));
modal.show();
} else {
alert('Failed to load result options: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error loading result options:', error);
alert('Error loading result options');
});
}
function addResultOption() {
const resultName = document.getElementById('result-option-name').value.trim().toUpperCase();
const resultDescription = document.getElementById('result-option-description').value.trim();
if (!resultName) {
alert('Result option name is required');
return;
}
if (resultName.length > 50) {
alert('Result option name must be 50 characters or less');
return;
}
fetch('/api/extraction/result-options/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
result_name: resultName,
description: resultDescription
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal and reset form
const modal = bootstrap.Modal.getInstance(document.getElementById('addResultOptionModal'));
modal.hide();
document.getElementById('add-result-option-form').reset();
// Reload results config to show new option
loadResultsConfig();
alert('Result option added successfully!');
} else {
alert('Failed to add result option: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error adding result option:', error);
alert('Error adding result option');
});
}
function deleteResultOption(resultName) {
if (!resultName) {
alert('Result name is required');
return;
}
if (!confirm(`Are you sure you want to delete the result option "${resultName}"?`)) {
return;
}
fetch('/api/extraction/result-options/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
result_name: resultName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Reload results config to remove deleted option
loadResultsConfig();
alert('Result option deleted successfully!');
} else {
alert('Failed to delete result option: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error deleting result option:', error);
alert('Error deleting result option');
});
}
// Make function globally available for onclick handlers
window.deleteResultOption = deleteResultOption;
function deleteResultOptionFromModal() {
const select = document.getElementById('delete-result-option-select');
const resultName = select.value;
if (!resultName) {
alert('Please select a result option to delete');
return;
}
deleteResultOption(resultName);
// Close modal
const modal = bootstrap.Modal.getInstance(document.getElementById('deleteResultOptionModal'));
modal.hide();
}
// Event listeners for result option modals
document.getElementById('btn-save-result-option').addEventListener('click', addResultOption);
document.getElementById('btn-confirm-delete-result-option').addEventListener('click', deleteResultOptionFromModal);
}); });
</script> </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