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):
# Insert default extraction associations
default_associations = [
('WIN1', 'WIN1', True),
('X1', 'WIN1', True),
('K01', 'WIN1', True),
('KO1', 'WIN1', True),
('RET1', 'WIN1', True),
('PTS1', 'WIN1', True),
('DRAW', 'X', True),
('12', 'X', True),
('X1', 'X', True),
('X2', 'X', True),
('DKO', 'X', True),
('WIN2', 'WIN2', True),
('X2', 'WIN2', True),
('K02', 'WIN2', True),
('KO2', 'WIN2', True),
('RET2', 'WIN2', True),
('PTS2', 'WIN2', True),
# DKO associations
('DRAW', 'DKO', True),
('X1', 'DKO', True),
('X2', 'DKO', True),
('DKO', 'DKO', True),
# DRAW associations
('DRAW', 'DRAW', True),
('X1', 'DRAW', True),
('X2', 'DRAW', True),
# KO1 associations
('KO1', 'KO1', True),
('WIN1', 'KO1', True),
('X1', 'KO1', True),
('12', 'KO1', True),
# KO2 associations
('KO2', 'KO2', 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:
......@@ -1845,14 +1867,12 @@ class Migration_024_AddResultOptionsTable(DatabaseMigration):
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),
('RET1', 'Fighter 1 retires result', 3),
('RET2', 'Fighter 2 retires result', 4),
('PTS1', 'Fighter 1 wins by points result', 5),
('PTS2', 'Fighter 2 wins by points result', 6),
('KO1', 'Fighter 1 wins by KO result', 7),
('KO2', 'Fighter 2 wins by KO result', 8),
]
for result_name, description, sort_order in default_results:
......
......@@ -2740,47 +2740,6 @@ def get_extraction_associations():
associations = session.query(ExtractionAssociationModel).all()
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({
"success": True,
"associations": associations_data,
......@@ -3495,9 +3454,9 @@ def add_available_bet():
@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
def delete_available_bet():
"""Delete an available bet"""
"""Delete an available bet and all associated associations"""
try:
from ..database.models import AvailableBetModel
from ..database.models import AvailableBetModel, ExtractionAssociationModel
data = request.get_json() or {}
bet_name = data.get('bet_name', '').strip().upper()
......@@ -3512,15 +3471,21 @@ def delete_available_bet():
if not bet:
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
session.delete(bet)
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({
"success": True,
"message": f"Bet '{bet_name}' deleted successfully"
"message": f"Bet '{bet_name}' deleted successfully",
"associations_removed": associations_deleted
})
finally:
......@@ -3615,9 +3580,9 @@ def add_result_option():
@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"""
"""Delete a result option and all associated associations"""
try:
from ..database.models import ResultOptionModel
from ..database.models import ResultOptionModel, ExtractionAssociationModel
data = request.get_json() or {}
result_name = data.get('result_name', '').strip().upper()
......@@ -3632,15 +3597,21 @@ def delete_result_option():
if not option:
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
session.delete(option)
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({
"success": True,
"message": f"Result option '{result_name}' deleted successfully"
"message": f"Result option '{result_name}' deleted successfully",
"associations_removed": associations_deleted
})
finally:
......
......@@ -617,7 +617,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) {
availableOutcomes = data.bets.map(bet => bet.bet_name);
displayOutcomesPool();
createOutcomeColumns();
} else {
showError('outcomes-pool', 'Failed to load bets');
}
......@@ -681,45 +680,75 @@ document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('outcome-columns');
container.innerHTML = '';
// Show columns for result options that are in Results area
if (resultsOutcomes.length === 0) {
container.innerHTML = `
<div class="text-center text-muted p-4">
<i class="fas fa-info-circle me-2"></i>
Add result options to the "Results" area on the left to create association columns here
</div>
`;
return;
// Group associations by extraction_result (the column headers)
const associationsByResult = {};
currentAssociations.forEach(assoc => {
if (!associationsByResult[assoc.extraction_result]) {
associationsByResult[assoc.extraction_result] = [];
}
associationsByResult[assoc.extraction_result].push(assoc.outcome_name);
});
resultsOutcomes.forEach(outcome => {
// Load ALL result options from database to create columns
fetch('/api/extraction/result-options')
.then(response => response.json())
.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 = outcome;
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">
${outcome}
${resultOption.result_name}
</div>
<div class="outcome-column-body" data-drop-target="${outcome}">
<div class="empty-column-message">
<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 ${outcome}
</div>
Drop bets here to associate with ${resultOption.result_name}
</div>` : ''
}
</div>
`;
container.appendChild(column);
});
// Now populate the columns with associations
displayAssociations();
} else {
container.innerHTML = `
<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>
`;
}
})
.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>
`;
});
}
function loadCurrentAssociations() {
fetch('/api/extraction/associations')
return fetch('/api/extraction/associations')
.then(response => response.json())
.then(data => {
if (data.success) {
currentAssociations = data.associations;
displayAssociations();
// Note: createOutcomeColumns and displayAssociations are now called from loadResultsConfig
// after resultsOutcomes is loaded
updateUnderOverZones();
} else {
console.error('Failed to load associations');
......@@ -731,36 +760,47 @@ document.addEventListener('DOMContentLoaded', function() {
}
function displayAssociations() {
// Clear all column bodies
document.querySelectorAll('.outcome-column-body').forEach(body => {
const message = body.querySelector('.empty-column-message');
body.innerHTML = '';
if (message) body.appendChild(message);
});
// Group associations by outcome
const associationsByOutcome = {};
// Group associations by extraction_result (the column headers)
const associationsByResult = {};
currentAssociations.forEach(assoc => {
if (!associationsByOutcome[assoc.outcome_name]) {
associationsByOutcome[assoc.outcome_name] = [];
if (!associationsByResult[assoc.extraction_result]) {
associationsByResult[assoc.extraction_result] = [];
}
associationsByOutcome[assoc.outcome_name].push(assoc.extraction_result);
associationsByResult[assoc.extraction_result].push(assoc.outcome_name);
});
// Display associations in columns
Object.keys(associationsByOutcome).forEach(outcome => {
const column = document.querySelector(`[data-outcome="${outcome}"] .outcome-column-body`);
// Update each column based on current associations
resultsOutcomes.forEach(resultOption => {
const column = document.querySelector(`[data-outcome="${resultOption}"] .outcome-column-body`);
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');
if (associatedBets.length === 0) {
// Show empty message if no associations
if (!emptyMessage) {
const newEmptyMessage = document.createElement('div');
newEmptyMessage.className = 'empty-column-message';
newEmptyMessage.innerHTML = `
<i class="fas fa-arrow-down me-2"></i>
Drop bets here to associate with ${resultOption}
`;
column.appendChild(newEmptyMessage);
}
} else {
// Remove empty message and add associations
if (emptyMessage) emptyMessage.remove();
// Add associated outcomes
associationsByOutcome[outcome].forEach(associatedOutcome => {
associatedBets.forEach(associatedOutcome => {
const associatedElement = document.createElement('div');
associatedElement.className = 'associated-outcome';
associatedElement.draggable = true;
associatedElement.dataset.outcome = outcome;
associatedElement.dataset.outcome = resultOption;
associatedElement.dataset.associated = associatedOutcome;
associatedElement.textContent = associatedOutcome;
......@@ -770,13 +810,14 @@ document.addEventListener('DOMContentLoaded', function() {
removeBtn.innerHTML = '×';
removeBtn.onclick = (e) => {
e.stopPropagation();
removeAssociation(outcome, associatedOutcome);
removeAssociation(resultOption, associatedOutcome);
};
associatedElement.appendChild(removeBtn);
column.appendChild(associatedElement);
});
}
}
});
}
......@@ -904,9 +945,6 @@ document.addEventListener('DOMContentLoaded', function() {
const draggedOutcome = draggedElement.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
if (!resultsOutcomes.includes(targetOutcome)) return false;
......@@ -956,7 +994,9 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
loadCurrentAssociations();
loadCurrentAssociations().then(() => {
displayAssociations();
});
} else {
alert('Failed to add association: ' + (data.error || 'Unknown error'));
}
......@@ -979,7 +1019,9 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
loadCurrentAssociations();
loadCurrentAssociations().then(() => {
displayAssociations();
});
} else {
alert('Failed to remove association: ' + (data.error || 'Unknown error'));
}
......@@ -1020,8 +1062,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (!resultsOutcomes.includes(outcome)) {
resultsOutcomes.push(outcome);
updateResultsPool();
createOutcomeColumns(); // Create new column for this outcome
displayAssociations(); // Refresh associations display
saveResultsConfig(); // Persist changes
}
}
......@@ -1082,8 +1122,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
updateResultsPool();
createOutcomeColumns(); // Recreate columns
displayAssociations(); // Refresh associations display
saveResultsConfig(); // Persist changes
};
......@@ -1103,7 +1141,12 @@ document.addEventListener('DOMContentLoaded', function() {
);
Promise.all(removePromises)
.then(() => loadCurrentAssociations())
.then(() => {
// Clear local associations and refresh display
currentAssociations = [];
displayAssociations();
updateUnderOverZones();
})
.catch(error => {
console.error('Error clearing associations:', error);
alert('Error clearing associations');
......@@ -1202,7 +1245,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Convert result options to the format expected by the UI
resultsOutcomes = data.options.map(option => option.result_name);
updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns();
// Redisplay associations
displayAssociations();
} else {
console.error('Failed to load result options:', data.error);
......@@ -1213,7 +1258,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (fallbackData.success) {
resultsOutcomes = fallbackData.results_outcomes || [];
updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns();
// Redisplay associations
displayAssociations();
}
})
......@@ -1229,7 +1276,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) {
resultsOutcomes = data.results_outcomes || [];
updateResultsPool();
// Recreate columns now that we have result options
createOutcomeColumns();
// Redisplay associations
displayAssociations();
}
})
......@@ -1450,7 +1499,9 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) {
// Reload available bets
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 {
alert('Failed to delete bet: ' + (data.error || 'Unknown error'));
}
......@@ -1532,8 +1583,6 @@ document.addEventListener('DOMContentLoaded', function() {
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'));
......@@ -1567,7 +1616,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success) {
// Reload results config to remove deleted option
loadResultsConfig();
alert('Result option deleted successfully!');
alert(`Result option deleted successfully! ${data.associations_removed || 0} associated associations were also removed.`);
} else {
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