Percentage system overhaul

This commit is contained in:
Dayowe 2025-06-26 12:21:18 +02:00
parent e789f481f3
commit 0a7020cb88

View File

@ -1657,7 +1657,7 @@
this.redistributePercentages();
this.renderFoodSource(foodSource);
this.updateAddButton();
this.updateFoodCalculations();
this.refreshAllPercentageUI();
}
removeFoodSource(id) {
@ -1680,7 +1680,7 @@
this.redistributePercentages();
this.updateFoodSourceNames();
this.updateAddButton();
this.updateFoodCalculations();
this.refreshAllPercentageUI();
}
generateFoodSourceId() {
@ -1711,9 +1711,13 @@
}
// Update the UI sliders and inputs
this.updatePercentageInputs();
this.refreshAllPercentageUI();
}
// OBSOLETE METHODS - Replaced by new validation system
// Keeping for reference but these are no longer used
/*
updatePercentageInputs() {
this.foodSources.forEach(fs => {
const slider = document.getElementById(`percentage-slider-${fs.id}`);
@ -1836,6 +1840,187 @@
this.updatePercentageInputs();
this.updateFoodCalculations();
}
*/
// New validation system methods
validatePercentageChange(sourceId, requestedValue) {
// Find the source being changed
const changedSource = this.foodSources.find(fs => fs.id === sourceId);
if (!changedSource) {
return { isValid: false, reason: 'Source not found' };
}
// If the source is locked, no change allowed
if (changedSource.isLocked) {
return { isValid: false, reason: 'Source is locked' };
}
// Ensure requested value is within bounds
const clampedValue = Math.max(0, Math.min(100, requestedValue));
// Calculate locked and other unlocked totals
const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked);
const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
// Check if the only unlocked source
if (otherUnlockedSources.length === 0) {
// This is the only unlocked source, must fill remaining percentage
const requiredPercentage = 100 - totalLocked;
return {
isValid: true,
actualValue: requiredPercentage,
affectedSources: [{ id: sourceId, newPercentage: requiredPercentage }],
reason: 'Only unlocked source, forced to fill remainder'
};
}
// Calculate available percentage for redistribution
const availableForOthers = 100 - clampedValue - totalLocked;
// Check if redistribution is possible
if (availableForOthers < 0) {
// Cannot accommodate this value
const maxAllowed = 100 - totalLocked;
return {
isValid: true,
actualValue: maxAllowed,
affectedSources: this.calculateRedistribution(sourceId, maxAllowed, otherUnlockedSources),
reason: 'Value clamped to maximum allowed'
};
}
// Calculate redistribution
const affectedSources = this.calculateRedistribution(sourceId, clampedValue, otherUnlockedSources);
return {
isValid: true,
actualValue: clampedValue,
affectedSources: affectedSources,
reason: 'Valid change'
};
}
calculateRedistribution(sourceId, newValue, otherUnlockedSources) {
const result = [{ id: sourceId, newPercentage: newValue }];
if (otherUnlockedSources.length === 0) {
return result;
}
// Calculate total locked percentage
const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
// Available percentage for other unlocked sources
const availableForOthers = 100 - newValue - totalLocked;
// Current total of other unlocked sources
const currentOtherTotal = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
if (currentOtherTotal === 0 || availableForOthers === 0) {
// Distribute equally among other unlocked sources
const equalShare = Math.floor(availableForOthers / otherUnlockedSources.length);
const remainder = availableForOthers - (equalShare * otherUnlockedSources.length);
otherUnlockedSources.forEach((fs, index) => {
const newPercentage = equalShare + (index < remainder ? 1 : 0);
result.push({ id: fs.id, newPercentage });
});
} else {
// Distribute proportionally
const scale = availableForOthers / currentOtherTotal;
let distributedTotal = 0;
otherUnlockedSources.forEach((fs, index) => {
let newPercentage;
if (index === otherUnlockedSources.length - 1) {
// Last item gets remainder to ensure exact total
newPercentage = availableForOthers - distributedTotal;
} else {
newPercentage = Math.round(fs.percentage * scale);
distributedTotal += newPercentage;
}
result.push({ id: fs.id, newPercentage });
});
}
return result;
}
applyValidatedChanges(validationResult) {
if (!validationResult.isValid) {
return false;
}
// Apply all percentage changes
validationResult.affectedSources.forEach(change => {
const source = this.foodSources.find(fs => fs.id === change.id);
if (source) {
source.percentage = change.newPercentage;
}
});
return true;
}
refreshAllPercentageUI() {
this.foodSources.forEach(fs => {
// Update all UI elements from single source of truth
const slider = document.getElementById(`percentage-slider-${fs.id}`);
const input = document.getElementById(`percentage-input-${fs.id}`);
const display = document.getElementById(`percentage-display-${fs.id}`);
if (slider) slider.value = fs.percentage;
if (input) input.value = fs.percentage;
if (display) display.textContent = `${fs.percentage}%`;
// Update constraints and disabled states
this.updateSliderConstraints(fs);
});
// Update food calculations
this.updateFoodCalculations();
}
updateSliderConstraints(foodSource) {
const slider = document.getElementById(`percentage-slider-${foodSource.id}`);
const input = document.getElementById(`percentage-input-${foodSource.id}`);
if (!slider || !input) return;
// Always keep 0-100 scale
slider.max = 100;
input.max = 100;
if (foodSource.isLocked) {
slider.disabled = true;
input.disabled = true;
} else {
// Calculate maximum allowed and store for validation
const maxAllowed = this.calculateMaxAllowed(foodSource.id);
slider.disabled = (maxAllowed <= 0);
input.disabled = (maxAllowed <= 0);
slider.dataset.maxAllowed = maxAllowed;
input.dataset.maxAllowed = maxAllowed;
}
}
calculateMaxAllowed(sourceId) {
const lockedSources = this.foodSources.filter(fs => fs.id !== sourceId && fs.isLocked);
const otherUnlockedSources = this.foodSources.filter(fs => fs.id !== sourceId && !fs.isLocked);
const totalLocked = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
// If this is the only unlocked source, it must take up the remainder
if (otherUnlockedSources.length === 0) {
return 100 - totalLocked;
}
// Otherwise, maximum is 100 minus locked percentages
return Math.max(0, 100 - totalLocked);
}
updateFoodSourceNames() {
this.foodSources.forEach((fs, index) => {
@ -1940,65 +2125,26 @@
if (percentageSlider) {
percentageSlider.addEventListener('input', () => {
// Check if this source is locked first
const foodSource = this.foodSources.find(fs => fs.id === id);
if (foodSource && foodSource.isLocked) {
// Reset slider to original value and return
percentageSlider.value = foodSource.percentage;
return;
const requestedValue = parseInt(percentageSlider.value);
const result = this.validatePercentageChange(id, requestedValue);
if (result.isValid) {
this.applyValidatedChanges(result);
}
// Check if this source has no flexibility (either only unlocked or no percentage available)
const otherUnlockedSources = this.foodSources.filter((fs, index) =>
fs.id !== id && !fs.isLocked
);
// Calculate available percentage for this source
const lockedSources = this.foodSources.filter(fs => fs.id !== id && fs.isLocked);
const totalLockedPercentage = lockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
const otherUnlockedPercentage = otherUnlockedSources.reduce((sum, fs) => sum + fs.percentage, 0);
const availablePercentage = 100 - totalLockedPercentage - otherUnlockedPercentage;
if (otherUnlockedSources.length === 0 || availablePercentage <= 0) {
// This source has no flexibility - don't update display, just call adjustPercentages
// which will force it to the correct value and update display properly
this.adjustPercentages(id, parseInt(percentageSlider.value));
return;
}
let newPercentage = parseInt(percentageSlider.value);
const maxAllowed = parseInt(percentageSlider.dataset.maxAllowed) || 100;
// Constrain to max allowed but keep slider scale 0-100
if (newPercentage > maxAllowed) {
newPercentage = maxAllowed;
percentageSlider.value = maxAllowed;
}
this.adjustPercentages(id, newPercentage);
document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
// Always refresh to ensure valid state
this.refreshAllPercentageUI();
});
}
if (percentageInput) {
percentageInput.addEventListener('change', () => {
// Check if this source is locked first
const foodSource = this.foodSources.find(fs => fs.id === id);
if (foodSource && foodSource.isLocked) {
// Reset input to original value and return
percentageInput.value = foodSource.percentage;
return;
const requestedValue = parseInt(percentageInput.value) || 0;
const result = this.validatePercentageChange(id, requestedValue);
if (result.isValid) {
this.applyValidatedChanges(result);
}
let newPercentage = parseInt(percentageInput.value) || 0;
const maxAllowed = parseInt(percentageInput.dataset.maxAllowed) || 100;
// Constrain to valid range and max allowed
newPercentage = Math.max(0, Math.min(maxAllowed, newPercentage));
percentageInput.value = newPercentage;
this.adjustPercentages(id, newPercentage);
document.getElementById(`percentage-display-${id}`).textContent = `${newPercentage}%`;
this.refreshAllPercentageUI();
});
}
@ -2027,6 +2173,7 @@
foodSource.isLocked = !foodSource.isLocked;
this.updateLockIcon(id);
this.updateLockStates();
this.refreshAllPercentageUI();
}
updateLockIcon(id) {
@ -2064,7 +2211,7 @@
});
// Update percentage constraints based on lock states
this.updatePercentageConstraints();
this.refreshAllPercentageUI();
}
updateFoodSourceData(id, field, value) {