diff --git a/iframe.html b/iframe.html index bd2d3e1..9f2010a 100644 --- a/iframe.html +++ b/iframe.html @@ -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) {