• feat(kaya): use current weight for Fred & Felia MER grams

- Add current weight input + validation to Kaya UI
  - Persist/restore weight in local storage
  - Compute Fred & Felia grams/day from MER using entered weight and age factor
  - Keep kibble chart-based; allow editing Fred energy density
  - Rebuild iframe.html from src/
This commit is contained in:
Dayowe
2026-01-28 15:51:28 +01:00
parent d0a30a8f8d
commit 19592f2230
6 changed files with 246 additions and 75 deletions
+6
View File
@@ -10,6 +10,12 @@
<div id="ageClampNote" class="dog-calculator-error dog-calculator-hidden">Age adjusted to the supported 212 month range.</div>
</div>
<div class="dog-calculator-form-group">
<label for="weight" id="weightLabel">Kayas current weight (kg):</label>
<input type="number" id="weight" min="0.1" step="0.1" placeholder="Enter current weight in kg" aria-describedby="weightHelp">
<div id="weightError" class="dog-calculator-error dog-calculator-hidden">Please enter a valid weight (minimum 0.1 kg)</div>
</div>
<div class="dog-calculator-form-group">
+69 -36
View File
@@ -17,7 +17,7 @@
this.showPerMeal = false;
// Kayafied reference source tracking
this.kibbleRefId = null;
this.gcRefId = null;
this.fredRefId = null;
this.storageKey = 'kaya_calculator_state_v1';
this.init();
}
@@ -42,11 +42,13 @@
saveStateToStorage() {
try {
const ageInput = document.getElementById('ageMonths');
const weightInput = document.getElementById('weight');
const unitSelect = document.getElementById('unit');
const daysInput = document.getElementById('days');
const state = {
version: 1,
age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null,
weight: weightInput && weightInput.value !== '' ? parseFloat(weightInput.value) : null,
unit: unitSelect ? unitSelect.value : 'g',
days: daysInput && daysInput.value ? parseInt(daysInput.value) : 1,
showPerMeal: !!this.showPerMeal,
@@ -115,12 +117,20 @@
ageInput.value = state.age;
}
// Restore weight (used for Fred & Felia MER calculation)
const weightInput = document.getElementById('weight');
if (weightInput && (state.weight || state.weight === 0)) {
weightInput.value = state.weight;
}
// Restore food sources
if (Array.isArray(state.foodSources) && state.foodSources.length) {
this.foodSources = [];
this.kibbleRefId = null;
this.gcRefId = null;
this.fredRefId = null;
state.foodSources.forEach(saved => {
// Back-compat: older Kaya state stored Fred & Felia as `chartType: "gc"`.
const chartType = saved.chartType === 'gc' ? 'mer' : (saved.chartType || null);
const fs = {
id: saved.id || this.generateFoodSourceId(),
name: saved.name || 'Food Source',
@@ -128,13 +138,13 @@
energyUnit: saved.energyUnit || 'kcal100g',
percentage: typeof saved.percentage === 'number' ? saved.percentage : 0,
isLocked: !!saved.isLocked,
chartType: saved.chartType || null,
chartType: chartType,
splitByMeals: (saved.splitByMeals === undefined ? true : saved.splitByMeals)
};
this.foodSources.push(fs);
this.renderFoodSource(fs);
if (fs.chartType === 'kibble' && !this.kibbleRefId) this.kibbleRefId = fs.id;
if (fs.chartType === 'gc' && !this.gcRefId) this.gcRefId = fs.id;
if (fs.chartType === 'mer' && !this.fredRefId) this.fredRefId = fs.id;
});
this.updateAddButton();
this.updateRemoveButtons();
@@ -188,18 +198,18 @@
// Food Source Management Methods
initializeFoodSources() {
// Seed three sources for Kaya's transition
const gc = {
const fred = {
id: this.generateFoodSourceId(),
name: 'Fred & Felia (Junior Huhn)',
energy: '115',
energyUnit: 'kcal100g',
percentage: 5,
isLocked: false,
chartType: 'gc'
chartType: 'mer'
};
this.foodSources.push(gc);
this.renderFoodSource(gc);
this.gcRefId = gc.id;
this.foodSources.push(fred);
this.renderFoodSource(fred);
this.fredRefId = fred.id;
const kibble = {
id: this.generateFoodSourceId(),
@@ -267,7 +277,7 @@
this.foodSources.splice(index, 1);
// If reference IDs were removed, clear them
if (this.kibbleRefId === id) this.kibbleRefId = null;
if (this.gcRefId === id) this.gcRefId = null;
if (this.fredRefId === id) this.fredRefId = null;
// Remove the DOM element
const element = document.getElementById(`foodSource-${id}`);
@@ -678,7 +688,7 @@
const container = document.getElementById('foodSources');
if (!container) return;
const isChart = foodSource.chartType === 'gc' || foodSource.chartType === 'kibble';
const isChart = foodSource.chartType === 'kibble';
const energyReadonlyAttr = isChart ? 'readonly' : '';
const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content';
const unitDisabledAttr = isChart ? 'disabled' : '';
@@ -969,8 +979,8 @@
const addFoodBtn = document.getElementById('addFoodBtn');
if (weightInput) {
weightInput.addEventListener('input', () => this.updateCalorieCalculations());
weightInput.addEventListener('blur', () => this.validateWeight());
weightInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
weightInput.addEventListener('blur', () => { this.validateWeight(); this.saveStateToStorage(); });
}
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
@@ -1335,26 +1345,38 @@
return lowerVal + (upperVal - lowerVal) * t;
}
// Kaya: GC chart interpolation across buckets
getGCChartGramsForAge(ageMonths) {
// Buckets per guideline with boundary rule (5.0 → later bucket, 7.0 → later bucket)
// <5 mo (2.0<5.0): 950→1350
// 56 mo (5.06.0): 1250→1550; (6.0<7.0): hold 1550
// 712 mo (7.012.0): 1300→1500
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const age = clamp(ageMonths, 2, 12);
if (age < 5) {
const t = (age - 2) / (5 - 2);
return 950 + t * (1350 - 950);
} else if (age < 6) {
const t = (age - 5) / (6 - 5);
return 1250 + t * (1550 - 1250);
} else if (age < 7) {
return 1550; // hold upper value until 7.0
} else {
const t = (age - 7) / (12 - 7);
return 1300 + t * (1500 - 1300);
}
// Kaya: MER-based grams/day for Fred & Felia (kibble remains chart-based)
getKayaMerFactorForAge(ageMonths) {
if (ageMonths === null || ageMonths === undefined) return null;
if (ageMonths < 4) return CALCULATOR_CONFIG.kayaMerFactorUnder4Months;
return CALCULATOR_CONFIG.kayaMerFactorFrom4Months;
}
getKayaCurrentWeightKg() {
const weightInput = document.getElementById('weight');
if (!weightInput) return CALCULATOR_CONFIG.kayaMerDefaultWeightKg;
const weightKg = this.getWeightInKg();
if (!weightKg || weightKg < 0.1) return null;
return weightKg;
}
getKayaMerCaloriesForAge(ageMonths) {
const factor = this.getKayaMerFactorForAge(ageMonths);
if (!factor) return null;
const weightKg = this.getKayaCurrentWeightKg();
if (!weightKg) return null;
const rer = this.calculateRER(weightKg);
return this.calculateMER(rer, factor);
}
getKayaMerBasedGramsForFood(ageMonths, energyPer100g) {
if (!energyPer100g || energyPer100g <= 0.1) return null;
const merCalories = this.getKayaMerCaloriesForAge(ageMonths);
if (!merCalories) return null;
const kcalPerGram = energyPer100g / 100;
return merCalories / kcalPerGram;
}
@@ -1413,6 +1435,7 @@
this.showError(`energy-error-${fs.id}`, false);
});
this.showError('daysError', false);
this.showError('weightError', false);
// Validate days input
if (!days || !this.validateInput(days, 1, true)) {
@@ -1441,6 +1464,16 @@
}
}
// Fred & Felia (MER-based) needs current weight to compute grams.
const needsWeight = this.foodSources.some(fs => fs.chartType === 'mer' && (fs.percentage || 0) > 0);
const weightInput = document.getElementById('weight');
if (needsWeight && weightInput) {
const weightKg = this.getWeightInKg();
if (!weightKg || weightKg < 0.1) {
this.showError('weightError', true);
}
}
// Calculate per-food breakdown (chart-first)
const foodBreakdowns = [];
let totalDailyGrams = 0;
@@ -1453,8 +1486,8 @@
this.foodSources.forEach(fs => {
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
let chartGrams = null;
if (fs.chartType === 'gc') {
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : null;
if (fs.chartType === 'mer') {
chartGrams = age !== null ? this.getKayaMerBasedGramsForFood(age, energyPer100g) : null;
} else if (fs.chartType === 'kibble') {
chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null;
}
@@ -1474,7 +1507,7 @@
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
let dailyGramsForThisFood = 0;
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
if ((fs.chartType === 'gc' || fs.chartType === 'kibble')) {
if ((fs.chartType === 'mer' || fs.chartType === 'kibble')) {
if (gramsPortion !== null) {
dailyGramsForThisFood = gramsPortion;
}
+8 -2
View File
@@ -7,5 +7,11 @@ const CALCULATOR_CONFIG = {
defaultScale: 1.0,
maxFoodSources: 5,
minScale: 0.5,
maxScale: 2.0
};
maxScale: 2.0,
// Kaya fork: Fred & Felia uses MER; kibble stays chart-based (30 kg column).
// Used only when weight input is not present (back-compat).
kayaMerDefaultWeightKg: 30,
kayaMerFactorUnder4Months: 3.0,
kayaMerFactorFrom4Months: 2.0
};