Fix extraction associations display in web interface

- Fixed displayAssociations() and createOutcomeColumns() functions to properly group associations by extraction_result instead of outcome_name
- This ensures all extraction associations are correctly displayed in their respective columns in the extraction management interface
parent bbd677ff
...@@ -924,23 +924,45 @@ class Migration_014_AddExtractionAndGameConfigTables(DatabaseMigration): ...@@ -924,23 +924,45 @@ class Migration_014_AddExtractionAndGameConfigTables(DatabaseMigration):
# Insert default extraction associations # Insert default extraction associations
default_associations = [ default_associations = [
('WIN1', 'WIN1', True), # DKO associations
('X1', 'WIN1', True), ('DRAW', 'DKO', True),
('K01', 'WIN1', True), ('X1', 'DKO', True),
('KO1', 'WIN1', True), ('X2', 'DKO', True),
('RET1', 'WIN1', True), ('DKO', 'DKO', True),
('PTS1', 'WIN1', True), # DRAW associations
('DRAW', 'X', True), ('DRAW', 'DRAW', True),
('12', 'X', True), ('X1', 'DRAW', True),
('X1', 'X', True), ('X2', 'DRAW', True),
('X2', 'X', True), # KO1 associations
('DKO', 'X', True), ('KO1', 'KO1', True),
('WIN2', 'WIN2', True), ('WIN1', 'KO1', True),
('X2', 'WIN2', True), ('X1', 'KO1', True),
('K02', 'WIN2', True), ('12', 'KO1', True),
('KO2', 'WIN2', True), # KO2 associations
('RET2', 'WIN2', True), ('KO2', 'KO2', True),
('PTS2', 'WIN2', True), ('WIN2', 'KO2', True),
('X2', 'KO2', True),
('12', 'KO2', True),
# PTS1 associations
('X1', 'PTS1', True),
('12', 'PTS1', True),
('PTS1', 'PTS1', True),
('WIN1', 'PTS1', True),
# PTS2 associations
('X2', 'PTS2', True),
('12', 'PTS2', True),
('PTS2', 'PTS2', True),
('WIN2', 'PTS2', True),
# RET1 associations
('WIN2', 'RET1', True),
('X2', 'RET1', True),
('12', 'RET1', True),
('RET1', 'RET1', True),
# RET2 associations
('WIN1', 'RET2', True),
('X1', 'RET2', True),
('12', 'RET2', True),
('RET2', 'RET2', True),
] ]
for outcome_name, extraction_result, is_default in default_associations: for outcome_name, extraction_result, is_default in default_associations:
...@@ -1845,14 +1867,12 @@ class Migration_024_AddResultOptionsTable(DatabaseMigration): ...@@ -1845,14 +1867,12 @@ class Migration_024_AddResultOptionsTable(DatabaseMigration):
default_results = [ default_results = [
('DRAW', 'Draw result', 1), ('DRAW', 'Draw result', 1),
('DKO', 'Double Knockout result', 2), ('DKO', 'Double Knockout result', 2),
('WIN1', 'Fighter 1 wins result', 3), ('RET1', 'Fighter 1 retires result', 3),
('WIN2', 'Fighter 2 wins result', 4), ('RET2', 'Fighter 2 retires result', 4),
('RET1', 'Fighter 1 retires result', 5), ('PTS1', 'Fighter 1 wins by points result', 5),
('RET2', 'Fighter 2 retires result', 6), ('PTS2', 'Fighter 2 wins by points result', 6),
('PTS1', 'Fighter 1 wins by points result', 7), ('KO1', 'Fighter 1 wins by KO result', 7),
('PTS2', 'Fighter 2 wins by points result', 8), ('KO2', 'Fighter 2 wins by KO 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: for result_name, description, sort_order in default_results:
......
...@@ -2740,47 +2740,6 @@ def get_extraction_associations(): ...@@ -2740,47 +2740,6 @@ def get_extraction_associations():
associations = session.query(ExtractionAssociationModel).all() associations = session.query(ExtractionAssociationModel).all()
associations_data = [assoc.to_dict() for assoc in associations] associations_data = [assoc.to_dict() for assoc in associations]
# Apply default associations if none exist
if not associations_data:
# Get available outcomes from database
available_outcomes_query = session.query(MatchOutcomeModel.column_name).distinct()
available_outcomes = [row[0] for row in available_outcomes_query.all()]
# Define default associations
default_associations = {
'DRAW': ['X1', 'X2'],
'DKO': ['DRAW', 'X1', 'X2'],
'KO1': ['WIN1', 'X1', '12'],
'KO2': ['WIN2', 'X2', '12'],
'PTS1': ['WIN1', 'X1', '12'],
'PTS2': ['WIN2', 'X2', '12'],
'RET1': ['WIN1', 'X1', '12'],
'RET2': ['WIN2', 'X2', '12']
}
# Create default associations for outcomes that exist in database
created_associations = []
for outcome_name, extraction_results in default_associations.items():
if outcome_name in available_outcomes:
for extraction_result in extraction_results:
if extraction_result in available_outcomes:
association = ExtractionAssociationModel(
outcome_name=outcome_name,
extraction_result=extraction_result,
is_default=True
)
session.add(association)
created_associations.append({
'outcome_name': outcome_name,
'extraction_result': extraction_result,
'is_default': True
})
if created_associations:
session.commit()
logger.info(f"Applied {len(created_associations)} default extraction associations")
associations_data = created_associations
return jsonify({ return jsonify({
"success": True, "success": True,
"associations": associations_data, "associations": associations_data,
...@@ -3495,9 +3454,9 @@ def add_available_bet(): ...@@ -3495,9 +3454,9 @@ def add_available_bet():
@api_bp.route('/extraction/available-bets/delete', methods=['POST']) @api_bp.route('/extraction/available-bets/delete', methods=['POST'])
@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
def delete_available_bet(): def delete_available_bet():
"""Delete an available bet""" """Delete an available bet and all associated associations"""
try: try:
from ..database.models import AvailableBetModel from ..database.models import AvailableBetModel, ExtractionAssociationModel
data = request.get_json() or {} data = request.get_json() or {}
bet_name = data.get('bet_name', '').strip().upper() bet_name = data.get('bet_name', '').strip().upper()
...@@ -3512,15 +3471,21 @@ def delete_available_bet(): ...@@ -3512,15 +3471,21 @@ def delete_available_bet():
if not bet: if not bet:
return jsonify({"error": f"Bet '{bet_name}' not found"}), 404 return jsonify({"error": f"Bet '{bet_name}' not found"}), 404
# Delete all associations that use this bet as extraction_result
associations_deleted = session.query(ExtractionAssociationModel).filter_by(
extraction_result=bet_name
).delete()
# Delete the bet # Delete the bet
session.delete(bet) session.delete(bet)
session.commit() session.commit()
logger.info(f"Deleted available bet: {bet_name}") logger.info(f"Deleted available bet: {bet_name} and {associations_deleted} associated associations")
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Bet '{bet_name}' deleted successfully" "message": f"Bet '{bet_name}' deleted successfully",
"associations_removed": associations_deleted
}) })
finally: finally:
...@@ -3615,9 +3580,9 @@ def add_result_option(): ...@@ -3615,9 +3580,9 @@ def add_result_option():
@api_bp.route('/extraction/result-options/delete', methods=['POST']) @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 @api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def delete_result_option(): def delete_result_option():
"""Delete a result option""" """Delete a result option and all associated associations"""
try: try:
from ..database.models import ResultOptionModel from ..database.models import ResultOptionModel, ExtractionAssociationModel
data = request.get_json() or {} data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper() result_name = data.get('result_name', '').strip().upper()
...@@ -3632,15 +3597,21 @@ def delete_result_option(): ...@@ -3632,15 +3597,21 @@ def delete_result_option():
if not option: if not option:
return jsonify({"error": f"Result option '{result_name}' not found"}), 404 return jsonify({"error": f"Result option '{result_name}' not found"}), 404
# Delete all associations that use this result option
associations_deleted = session.query(ExtractionAssociationModel).filter_by(
extraction_result=result_name
).delete()
# Delete the result option # Delete the result option
session.delete(option) session.delete(option)
session.commit() session.commit()
logger.info(f"Deleted result option: {result_name}") logger.info(f"Deleted result option: {result_name} and {associations_deleted} associated associations")
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Result option '{result_name}' deleted successfully" "message": f"Result option '{result_name}' deleted successfully",
"associations_removed": associations_deleted
}) })
finally: finally:
......
...@@ -617,7 +617,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -617,7 +617,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
availableOutcomes = data.bets.map(bet => bet.bet_name); availableOutcomes = data.bets.map(bet => bet.bet_name);
displayOutcomesPool(); displayOutcomesPool();
createOutcomeColumns();
} else { } else {
showError('outcomes-pool', 'Failed to load bets'); showError('outcomes-pool', 'Failed to load bets');
} }
...@@ -681,45 +680,75 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -681,45 +680,75 @@ document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('outcome-columns'); const container = document.getElementById('outcome-columns');
container.innerHTML = ''; container.innerHTML = '';
// Show columns for result options that are in Results area // Group associations by extraction_result (the column headers)
if (resultsOutcomes.length === 0) { const associationsByResult = {};
container.innerHTML = ` currentAssociations.forEach(assoc => {
<div class="text-center text-muted p-4"> if (!associationsByResult[assoc.extraction_result]) {
<i class="fas fa-info-circle me-2"></i> associationsByResult[assoc.extraction_result] = [];
Add result options to the "Results" area on the left to create association columns here }
</div> associationsByResult[assoc.extraction_result].push(assoc.outcome_name);
`; });
return;
}
resultsOutcomes.forEach(outcome => { // Load ALL result options from database to create columns
const column = document.createElement('div'); fetch('/api/extraction/result-options')
column.className = 'outcome-column new'; .then(response => response.json())
column.dataset.outcome = outcome; .then(data => {
if (data.success && data.options.length > 0) {
data.options.forEach(resultOption => {
const column = document.createElement('div');
column.className = 'outcome-column new';
column.dataset.outcome = resultOption.result_name;
// Get associations for this result option
const associatedBets = associationsByResult[resultOption.result_name] || [];
column.innerHTML = `
<div class="outcome-column-header">
${resultOption.result_name}
</div>
<div class="outcome-column-body" data-drop-target="${resultOption.result_name}">
${associatedBets.length === 0 ?
`<div class="empty-column-message">
<i class="fas fa-arrow-down me-2"></i>
Drop bets here to associate with ${resultOption.result_name}
</div>` : ''
}
</div>
`;
column.innerHTML = ` container.appendChild(column);
<div class="outcome-column-header"> });
${outcome}
</div> // Now populate the columns with associations
<div class="outcome-column-body" data-drop-target="${outcome}"> displayAssociations();
<div class="empty-column-message"> } else {
<i class="fas fa-arrow-down me-2"></i> container.innerHTML = `
Drop bets here to associate with ${outcome} <div class="text-center text-muted p-4">
<i class="fas fa-info-circle me-2"></i>
No result options configured. Add result options to create association columns.
</div> </div>
`;
}
})
.catch(error => {
console.error('Error loading result options for columns:', error);
container.innerHTML = `
<div class="text-center text-muted p-4">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading result options.
</div> </div>
`; `;
container.appendChild(column);
}); });
} }
function loadCurrentAssociations() { function loadCurrentAssociations() {
fetch('/api/extraction/associations') return fetch('/api/extraction/associations')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
currentAssociations = data.associations; currentAssociations = data.associations;
displayAssociations(); // Note: createOutcomeColumns and displayAssociations are now called from loadResultsConfig
// after resultsOutcomes is loaded
updateUnderOverZones(); updateUnderOverZones();
} else { } else {
console.error('Failed to load associations'); console.error('Failed to load associations');
...@@ -731,51 +760,63 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -731,51 +760,63 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function displayAssociations() { function displayAssociations() {
// Clear all column bodies // Group associations by extraction_result (the column headers)
document.querySelectorAll('.outcome-column-body').forEach(body => { const associationsByResult = {};
const message = body.querySelector('.empty-column-message');
body.innerHTML = '';
if (message) body.appendChild(message);
});
// Group associations by outcome
const associationsByOutcome = {};
currentAssociations.forEach(assoc => { currentAssociations.forEach(assoc => {
if (!associationsByOutcome[assoc.outcome_name]) { if (!associationsByResult[assoc.extraction_result]) {
associationsByOutcome[assoc.outcome_name] = []; associationsByResult[assoc.extraction_result] = [];
} }
associationsByOutcome[assoc.outcome_name].push(assoc.extraction_result); associationsByResult[assoc.extraction_result].push(assoc.outcome_name);
}); });
// Display associations in columns // Update each column based on current associations
Object.keys(associationsByOutcome).forEach(outcome => { resultsOutcomes.forEach(resultOption => {
const column = document.querySelector(`[data-outcome="${outcome}"] .outcome-column-body`); const column = document.querySelector(`[data-outcome="${resultOption}"] .outcome-column-body`);
if (column) { if (column) {
// Remove empty message // Clear existing associations but keep the empty message if it exists
const existingAssociations = column.querySelectorAll('.associated-outcome');
existingAssociations.forEach(assoc => assoc.remove());
const associatedBets = associationsByResult[resultOption] || [];
const emptyMessage = column.querySelector('.empty-column-message'); const emptyMessage = column.querySelector('.empty-column-message');
if (emptyMessage) emptyMessage.remove();
if (associatedBets.length === 0) {
// Add associated outcomes // Show empty message if no associations
associationsByOutcome[outcome].forEach(associatedOutcome => { if (!emptyMessage) {
const associatedElement = document.createElement('div'); const newEmptyMessage = document.createElement('div');
associatedElement.className = 'associated-outcome'; newEmptyMessage.className = 'empty-column-message';
associatedElement.draggable = true; newEmptyMessage.innerHTML = `
associatedElement.dataset.outcome = outcome; <i class="fas fa-arrow-down me-2"></i>
associatedElement.dataset.associated = associatedOutcome; Drop bets here to associate with ${resultOption}
associatedElement.textContent = associatedOutcome; `;
column.appendChild(newEmptyMessage);
// Add remove button }
const removeBtn = document.createElement('button'); } else {
removeBtn.className = 'remove-btn'; // Remove empty message and add associations
removeBtn.innerHTML = '×'; if (emptyMessage) emptyMessage.remove();
removeBtn.onclick = (e) => {
e.stopPropagation(); // Add associated outcomes
removeAssociation(outcome, associatedOutcome); associatedBets.forEach(associatedOutcome => {
}; const associatedElement = document.createElement('div');
associatedElement.appendChild(removeBtn); associatedElement.className = 'associated-outcome';
associatedElement.draggable = true;
column.appendChild(associatedElement); associatedElement.dataset.outcome = resultOption;
}); associatedElement.dataset.associated = associatedOutcome;
associatedElement.textContent = associatedOutcome;
// Add remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.innerHTML = '×';
removeBtn.onclick = (e) => {
e.stopPropagation();
removeAssociation(resultOption, associatedOutcome);
};
associatedElement.appendChild(removeBtn);
column.appendChild(associatedElement);
});
}
} }
}); });
} }
...@@ -900,19 +941,16 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -900,19 +941,16 @@ document.addEventListener('DOMContentLoaded', function() {
function validateDrop(draggedElement, target) { function validateDrop(draggedElement, target) {
if (!draggedElement || !target.classList.contains('outcome-column')) return false; if (!draggedElement || !target.classList.contains('outcome-column')) return false;
const draggedOutcome = draggedElement.dataset.outcome; const draggedOutcome = draggedElement.dataset.outcome;
const targetOutcome = target.dataset.outcome; const targetOutcome = target.dataset.outcome;
// Can't associate outcome with itself
if (draggedOutcome === targetOutcome) return false;
// For outcome columns, target must be in Results area // For outcome columns, target must be in Results area
if (!resultsOutcomes.includes(targetOutcome)) return false; if (!resultsOutcomes.includes(targetOutcome)) return false;
// Dragged outcome must be from the available outcomes // Dragged outcome must be from the available outcomes
if (!availableOutcomes.includes(draggedOutcome)) return false; if (!availableOutcomes.includes(draggedOutcome)) return false;
// Check if association already exists // Check if association already exists
return !currentAssociations.some(assoc => return !currentAssociations.some(assoc =>
assoc.outcome_name === targetOutcome && assoc.extraction_result === draggedOutcome assoc.outcome_name === targetOutcome && assoc.extraction_result === draggedOutcome
...@@ -956,7 +994,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -956,7 +994,9 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
loadCurrentAssociations(); loadCurrentAssociations().then(() => {
displayAssociations();
});
} else { } else {
alert('Failed to add association: ' + (data.error || 'Unknown error')); alert('Failed to add association: ' + (data.error || 'Unknown error'));
} }
...@@ -979,7 +1019,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -979,7 +1019,9 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
loadCurrentAssociations(); loadCurrentAssociations().then(() => {
displayAssociations();
});
} else { } else {
alert('Failed to remove association: ' + (data.error || 'Unknown error')); alert('Failed to remove association: ' + (data.error || 'Unknown error'));
} }
...@@ -1020,8 +1062,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1020,8 +1062,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (!resultsOutcomes.includes(outcome)) { if (!resultsOutcomes.includes(outcome)) {
resultsOutcomes.push(outcome); resultsOutcomes.push(outcome);
updateResultsPool(); updateResultsPool();
createOutcomeColumns(); // Create new column for this outcome
displayAssociations(); // Refresh associations display
saveResultsConfig(); // Persist changes saveResultsConfig(); // Persist changes
} }
} }
...@@ -1082,8 +1122,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1082,8 +1122,6 @@ document.addEventListener('DOMContentLoaded', function() {
} }
updateResultsPool(); updateResultsPool();
createOutcomeColumns(); // Recreate columns
displayAssociations(); // Refresh associations display
saveResultsConfig(); // Persist changes saveResultsConfig(); // Persist changes
}; };
...@@ -1103,7 +1141,12 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1103,7 +1141,12 @@ document.addEventListener('DOMContentLoaded', function() {
); );
Promise.all(removePromises) Promise.all(removePromises)
.then(() => loadCurrentAssociations()) .then(() => {
// Clear local associations and refresh display
currentAssociations = [];
displayAssociations();
updateUnderOverZones();
})
.catch(error => { .catch(error => {
console.error('Error clearing associations:', error); console.error('Error clearing associations:', error);
alert('Error clearing associations'); alert('Error clearing associations');
...@@ -1202,7 +1245,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1202,7 +1245,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Convert result options to the format expected by the UI // Convert result options to the format expected by the UI
resultsOutcomes = data.options.map(option => option.result_name); resultsOutcomes = data.options.map(option => option.result_name);
updateResultsPool(); updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns(); createOutcomeColumns();
// Redisplay associations
displayAssociations(); displayAssociations();
} else { } else {
console.error('Failed to load result options:', data.error); console.error('Failed to load result options:', data.error);
...@@ -1213,7 +1258,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1213,7 +1258,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (fallbackData.success) { if (fallbackData.success) {
resultsOutcomes = fallbackData.results_outcomes || []; resultsOutcomes = fallbackData.results_outcomes || [];
updateResultsPool(); updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns(); createOutcomeColumns();
// Redisplay associations
displayAssociations(); displayAssociations();
} }
}) })
...@@ -1229,7 +1276,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1229,7 +1276,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
resultsOutcomes = data.results_outcomes || []; resultsOutcomes = data.results_outcomes || [];
updateResultsPool(); updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns(); createOutcomeColumns();
// Redisplay associations
displayAssociations(); displayAssociations();
} }
}) })
...@@ -1450,7 +1499,9 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1450,7 +1499,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
// Reload available bets // Reload available bets
loadAvailableOutcomes(); loadAvailableOutcomes();
alert('Bet deleted successfully!'); // Reload associations since some may have been removed
loadCurrentAssociations();
alert(`Bet deleted successfully! ${data.associations_removed || 0} associated associations were also removed.`);
} else { } else {
alert('Failed to delete bet: ' + (data.error || 'Unknown error')); alert('Failed to delete bet: ' + (data.error || 'Unknown error'));
} }
...@@ -1532,8 +1583,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1532,8 +1583,6 @@ document.addEventListener('DOMContentLoaded', function() {
modal.hide(); modal.hide();
document.getElementById('add-result-option-form').reset(); document.getElementById('add-result-option-form').reset();
// Reload results config to show new option
loadResultsConfig();
alert('Result option added successfully!'); alert('Result option added successfully!');
} else { } else {
alert('Failed to add result option: ' + (data.error || 'Unknown error')); alert('Failed to add result option: ' + (data.error || 'Unknown error'));
...@@ -1567,7 +1616,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -1567,7 +1616,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) { if (data.success) {
// Reload results config to remove deleted option // Reload results config to remove deleted option
loadResultsConfig(); loadResultsConfig();
alert('Result option deleted successfully!'); alert(`Result option deleted successfully! ${data.associations_removed || 0} associated associations were also removed.`);
} else { } else {
alert('Failed to delete result option: ' + (data.error || 'Unknown error')); alert('Failed to delete result option: ' + (data.error || 'Unknown error'));
} }
......
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