• 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

43
KAYA-2.md Normal file
View File

@ -0,0 +1,43 @@
Kayas Transition Calculator — Friendly Guide
============================================
This quick guide shows you how to use the calculator to move Kaya from kibble to gently cooked food at a steady, safe pace.
How to use it
-------------
1) Enter Kayas age in months (e.g. "5.5")
2) Check the energy numbers for your foods:
- Eukanuba (kibble) defaults to 372 kcal/100g (replace if your bag shows a different number)
- Fred & Felia (gently cooked) defaults to 115 kcal/100g (replace if your bag shows a different number)
- Add or rename the foods (e.g., “Treats”) and enter their energy values from the label (kcal/100 g, kcal/kg, kcal/cup, or kcal/can).
3) Set your percentages. We will start small (e.g., GC 5%, kibble 95%), lock any values you want fixed.
4) Choose how to view amounts:
- Per day or per meal; set meals/day if needed.
- Pick your preferred units (grams, kg).
- Use “days” to see totals for meal prep.
Regarding treats
-----
- Keep treats ≤10% (your plan is good). Larger shares can dilute the balanced portion of the diet.
- Enter treat kcal from the package (asfed) for accuracy.
- Monitor body condition and stool; adjust if needed.
What youll see
---------------
- Exact amounts for each food based on your percentages and energy labels.
- Totals per day (or per meal), and optional multiday batches.
- Everything updates instantly as you change inputs.
Good to know
------------
- Gently cooked is less caloriedense than kibble. As you add more GC, total grams may go up. This is normal.
- Treats count. I added a “Treats” source with its kcal so the plan stays balanced.
- When in doubt, doublecheck kcal values on the package (kcal/100 g or kcal/kg are most common).
Remember
-----------------
Every dog is unique. Monitor body condition, stool quality, and appetite. We will adjust percentages and amounts as Kaya grows and responds to the new plan.

38
KAYA.md Normal file
View File

@ -0,0 +1,38 @@
Kaya Transition Calculator — Quick Guide
=======================================
This tool helps transition Kaya (30 kg adult) from kibble to gently cooked while keeping daily energy intake continuous. Enter Kayas age, set food sources and their energy values, then adjust percentages to see exactly how much to feed of each food.
How to use
----------
- Enter Kayas age in months (2.012.0). If outside this range, the tool adjusts to the nearest valid value.
- Confirm energy values for each food source:
- “Eukanuba, kibble” defaults to 372 kcal/100 g — change if your bag shows a different value.
- “Fred & Felia, gently cooked” defaults to 115 kcal/100 g.
- Add/rename sources (e.g., “Treats”) and enter their energy (kcal/100 g, kcal/kg, kcal/cup, or kcal/can).
- Set percentages for each food; lock any you want fixed. The total always equals 100%.
- Optional: switch units (g/kg/oz/lb; cups enabled only when kcal/cup is entered), choose perday or permeal, set meals/day, and use “days” to see batch totals.
What it calculates
------------------
- Daily energy target is derived from the 30 kg kibble feeding curve using Kayas exact age (monthlevel interpolation), not from generic MER.
- That daily kcal target is split across your foods by percentage and converted into amounts using each foods energy density.
- Results include perfood amounts and totals, perday or permeal, in your selected units.
Formulas (at a glance)
----------------------
- Kibble grams/day (30 kg) by month (g/day):
2: 250, 3: 330, 4: 365, 5: 382, 6: 400, 7: 405, 8: 410, 9: 410, 10: 410, 11: 408, 12: 405.
Linear interpolation is applied between months (e.g., 5.5 months is halfway between 5 and 6).
- Daily kcal target = kibble_g/day × (kibble_kcal_per_100g ÷ 100)
- Perfood kcal = daily_kcal × (food_percentage ÷ 100)
- Perfood grams (kcal/100 g) = perfood_kcal ÷ (kcal_per_100g ÷ 100)
- Perfood grams (kcal/kg) = perfood_kcal ÷ (kcal_per_kg ÷ 1000)
- Cups (when kcal/cup provided) = perfood_kcal ÷ kcal_per_cup
- Internal assumptions for conversions: 1 cup ≈ 120 g (dry), 1 can ≈ 450 g (wet)
Notes
-----
- Age input is limited to 212 months; values are rounded for display (permeal amounts round after splitting).
- Accuracy depends on correct energy values on your foods labels. When in doubt, confirm the kcal numbers on the packaging.
- This guide supports professional planning; your nutritionist may finetune based on Kayas body condition and response.

View File

@ -1981,6 +1981,12 @@
<div id="ageClampNote" class="dog-calculator-error dog-calculator-hidden">Age adjusted to the supported 212 month range.</div> <div id="ageClampNote" class="dog-calculator-error dog-calculator-hidden">Age adjusted to the supported 212 month range.</div>
</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"> <div class="dog-calculator-form-group">
@ -2091,7 +2097,13 @@ const CALCULATOR_CONFIG = {
defaultScale: 1.0, defaultScale: 1.0,
maxFoodSources: 5, maxFoodSources: 5,
minScale: 0.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
}; };
/** /**
@ -2113,7 +2125,7 @@ const CALCULATOR_CONFIG = {
this.showPerMeal = false; this.showPerMeal = false;
// Kayafied reference source tracking // Kayafied reference source tracking
this.kibbleRefId = null; this.kibbleRefId = null;
this.gcRefId = null; this.fredRefId = null;
this.storageKey = 'kaya_calculator_state_v1'; this.storageKey = 'kaya_calculator_state_v1';
this.init(); this.init();
} }
@ -2138,11 +2150,13 @@ const CALCULATOR_CONFIG = {
saveStateToStorage() { saveStateToStorage() {
try { try {
const ageInput = document.getElementById('ageMonths'); const ageInput = document.getElementById('ageMonths');
const weightInput = document.getElementById('weight');
const unitSelect = document.getElementById('unit'); const unitSelect = document.getElementById('unit');
const daysInput = document.getElementById('days'); const daysInput = document.getElementById('days');
const state = { const state = {
version: 1, version: 1,
age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null, age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null,
weight: weightInput && weightInput.value !== '' ? parseFloat(weightInput.value) : null,
unit: unitSelect ? unitSelect.value : 'g', unit: unitSelect ? unitSelect.value : 'g',
days: daysInput && daysInput.value ? parseInt(daysInput.value) : 1, days: daysInput && daysInput.value ? parseInt(daysInput.value) : 1,
showPerMeal: !!this.showPerMeal, showPerMeal: !!this.showPerMeal,
@ -2211,12 +2225,20 @@ const CALCULATOR_CONFIG = {
ageInput.value = state.age; 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 // Restore food sources
if (Array.isArray(state.foodSources) && state.foodSources.length) { if (Array.isArray(state.foodSources) && state.foodSources.length) {
this.foodSources = []; this.foodSources = [];
this.kibbleRefId = null; this.kibbleRefId = null;
this.gcRefId = null; this.fredRefId = null;
state.foodSources.forEach(saved => { 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 = { const fs = {
id: saved.id || this.generateFoodSourceId(), id: saved.id || this.generateFoodSourceId(),
name: saved.name || 'Food Source', name: saved.name || 'Food Source',
@ -2224,13 +2246,13 @@ const CALCULATOR_CONFIG = {
energyUnit: saved.energyUnit || 'kcal100g', energyUnit: saved.energyUnit || 'kcal100g',
percentage: typeof saved.percentage === 'number' ? saved.percentage : 0, percentage: typeof saved.percentage === 'number' ? saved.percentage : 0,
isLocked: !!saved.isLocked, isLocked: !!saved.isLocked,
chartType: saved.chartType || null, chartType: chartType,
splitByMeals: (saved.splitByMeals === undefined ? true : saved.splitByMeals) splitByMeals: (saved.splitByMeals === undefined ? true : saved.splitByMeals)
}; };
this.foodSources.push(fs); this.foodSources.push(fs);
this.renderFoodSource(fs); this.renderFoodSource(fs);
if (fs.chartType === 'kibble' && !this.kibbleRefId) this.kibbleRefId = fs.id; 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.updateAddButton();
this.updateRemoveButtons(); this.updateRemoveButtons();
@ -2284,18 +2306,18 @@ const CALCULATOR_CONFIG = {
// Food Source Management Methods // Food Source Management Methods
initializeFoodSources() { initializeFoodSources() {
// Seed three sources for Kaya's transition // Seed three sources for Kaya's transition
const gc = { const fred = {
id: this.generateFoodSourceId(), id: this.generateFoodSourceId(),
name: 'Fred & Felia (Junior Huhn)', name: 'Fred & Felia (Junior Huhn)',
energy: '115', energy: '115',
energyUnit: 'kcal100g', energyUnit: 'kcal100g',
percentage: 5, percentage: 5,
isLocked: false, isLocked: false,
chartType: 'gc' chartType: 'mer'
}; };
this.foodSources.push(gc); this.foodSources.push(fred);
this.renderFoodSource(gc); this.renderFoodSource(fred);
this.gcRefId = gc.id; this.fredRefId = fred.id;
const kibble = { const kibble = {
id: this.generateFoodSourceId(), id: this.generateFoodSourceId(),
@ -2363,7 +2385,7 @@ const CALCULATOR_CONFIG = {
this.foodSources.splice(index, 1); this.foodSources.splice(index, 1);
// If reference IDs were removed, clear them // If reference IDs were removed, clear them
if (this.kibbleRefId === id) this.kibbleRefId = null; 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 // Remove the DOM element
const element = document.getElementById(`foodSource-${id}`); const element = document.getElementById(`foodSource-${id}`);
@ -2774,7 +2796,7 @@ const CALCULATOR_CONFIG = {
const container = document.getElementById('foodSources'); const container = document.getElementById('foodSources');
if (!container) return; if (!container) return;
const isChart = foodSource.chartType === 'gc' || foodSource.chartType === 'kibble'; const isChart = foodSource.chartType === 'kibble';
const energyReadonlyAttr = isChart ? 'readonly' : ''; const energyReadonlyAttr = isChart ? 'readonly' : '';
const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content'; const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content';
const unitDisabledAttr = isChart ? 'disabled' : ''; const unitDisabledAttr = isChart ? 'disabled' : '';
@ -3065,8 +3087,8 @@ const CALCULATOR_CONFIG = {
const addFoodBtn = document.getElementById('addFoodBtn'); const addFoodBtn = document.getElementById('addFoodBtn');
if (weightInput) { if (weightInput) {
weightInput.addEventListener('input', () => this.updateCalorieCalculations()); weightInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
weightInput.addEventListener('blur', () => this.validateWeight()); weightInput.addEventListener('blur', () => { this.validateWeight(); this.saveStateToStorage(); });
} }
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations()); if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
@ -3431,26 +3453,38 @@ const CALCULATOR_CONFIG = {
return lowerVal + (upperVal - lowerVal) * t; return lowerVal + (upperVal - lowerVal) * t;
} }
// Kaya: GC chart interpolation across buckets // Kaya: MER-based grams/day for Fred & Felia (kibble remains chart-based)
getGCChartGramsForAge(ageMonths) { getKayaMerFactorForAge(ageMonths) {
// Buckets per guideline with boundary rule (5.0 → later bucket, 7.0 → later bucket) if (ageMonths === null || ageMonths === undefined) return null;
// <5 mo (2.0<5.0): 9501350 if (ageMonths < 4) return CALCULATOR_CONFIG.kayaMerFactorUnder4Months;
// 56 mo (5.06.0): 1250→1550; (6.0<7.0): hold 1550 return CALCULATOR_CONFIG.kayaMerFactorFrom4Months;
// 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);
} }
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;
} }
@ -3509,6 +3543,7 @@ const CALCULATOR_CONFIG = {
this.showError(`energy-error-${fs.id}`, false); this.showError(`energy-error-${fs.id}`, false);
}); });
this.showError('daysError', false); this.showError('daysError', false);
this.showError('weightError', false);
// Validate days input // Validate days input
if (!days || !this.validateInput(days, 1, true)) { if (!days || !this.validateInput(days, 1, true)) {
@ -3537,6 +3572,16 @@ const CALCULATOR_CONFIG = {
} }
} }
// 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) // Calculate per-food breakdown (chart-first)
const foodBreakdowns = []; const foodBreakdowns = [];
let totalDailyGrams = 0; let totalDailyGrams = 0;
@ -3549,8 +3594,8 @@ const CALCULATOR_CONFIG = {
this.foodSources.forEach(fs => { this.foodSources.forEach(fs => {
const energyPer100g = this.getFoodSourceEnergyPer100g(fs); const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
let chartGrams = null; let chartGrams = null;
if (fs.chartType === 'gc') { if (fs.chartType === 'mer') {
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : null; chartGrams = age !== null ? this.getKayaMerBasedGramsForFood(age, energyPer100g) : null;
} else if (fs.chartType === 'kibble') { } else if (fs.chartType === 'kibble') {
chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null; chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null;
} }
@ -3570,7 +3615,7 @@ const CALCULATOR_CONFIG = {
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => { firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
let dailyGramsForThisFood = 0; let dailyGramsForThisFood = 0;
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0); let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
if ((fs.chartType === 'gc' || fs.chartType === 'kibble')) { if ((fs.chartType === 'mer' || fs.chartType === 'kibble')) {
if (gramsPortion !== null) { if (gramsPortion !== null) {
dailyGramsForThisFood = gramsPortion; dailyGramsForThisFood = gramsPortion;
} }

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 id="ageClampNote" class="dog-calculator-error dog-calculator-hidden">Age adjusted to the supported 212 month range.</div>
</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"> <div class="dog-calculator-form-group">

View File

@ -17,7 +17,7 @@
this.showPerMeal = false; this.showPerMeal = false;
// Kayafied reference source tracking // Kayafied reference source tracking
this.kibbleRefId = null; this.kibbleRefId = null;
this.gcRefId = null; this.fredRefId = null;
this.storageKey = 'kaya_calculator_state_v1'; this.storageKey = 'kaya_calculator_state_v1';
this.init(); this.init();
} }
@ -42,11 +42,13 @@
saveStateToStorage() { saveStateToStorage() {
try { try {
const ageInput = document.getElementById('ageMonths'); const ageInput = document.getElementById('ageMonths');
const weightInput = document.getElementById('weight');
const unitSelect = document.getElementById('unit'); const unitSelect = document.getElementById('unit');
const daysInput = document.getElementById('days'); const daysInput = document.getElementById('days');
const state = { const state = {
version: 1, version: 1,
age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null, age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null,
weight: weightInput && weightInput.value !== '' ? parseFloat(weightInput.value) : null,
unit: unitSelect ? unitSelect.value : 'g', unit: unitSelect ? unitSelect.value : 'g',
days: daysInput && daysInput.value ? parseInt(daysInput.value) : 1, days: daysInput && daysInput.value ? parseInt(daysInput.value) : 1,
showPerMeal: !!this.showPerMeal, showPerMeal: !!this.showPerMeal,
@ -115,12 +117,20 @@
ageInput.value = state.age; 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 // Restore food sources
if (Array.isArray(state.foodSources) && state.foodSources.length) { if (Array.isArray(state.foodSources) && state.foodSources.length) {
this.foodSources = []; this.foodSources = [];
this.kibbleRefId = null; this.kibbleRefId = null;
this.gcRefId = null; this.fredRefId = null;
state.foodSources.forEach(saved => { 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 = { const fs = {
id: saved.id || this.generateFoodSourceId(), id: saved.id || this.generateFoodSourceId(),
name: saved.name || 'Food Source', name: saved.name || 'Food Source',
@ -128,13 +138,13 @@
energyUnit: saved.energyUnit || 'kcal100g', energyUnit: saved.energyUnit || 'kcal100g',
percentage: typeof saved.percentage === 'number' ? saved.percentage : 0, percentage: typeof saved.percentage === 'number' ? saved.percentage : 0,
isLocked: !!saved.isLocked, isLocked: !!saved.isLocked,
chartType: saved.chartType || null, chartType: chartType,
splitByMeals: (saved.splitByMeals === undefined ? true : saved.splitByMeals) splitByMeals: (saved.splitByMeals === undefined ? true : saved.splitByMeals)
}; };
this.foodSources.push(fs); this.foodSources.push(fs);
this.renderFoodSource(fs); this.renderFoodSource(fs);
if (fs.chartType === 'kibble' && !this.kibbleRefId) this.kibbleRefId = fs.id; 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.updateAddButton();
this.updateRemoveButtons(); this.updateRemoveButtons();
@ -188,18 +198,18 @@
// Food Source Management Methods // Food Source Management Methods
initializeFoodSources() { initializeFoodSources() {
// Seed three sources for Kaya's transition // Seed three sources for Kaya's transition
const gc = { const fred = {
id: this.generateFoodSourceId(), id: this.generateFoodSourceId(),
name: 'Fred & Felia (Junior Huhn)', name: 'Fred & Felia (Junior Huhn)',
energy: '115', energy: '115',
energyUnit: 'kcal100g', energyUnit: 'kcal100g',
percentage: 5, percentage: 5,
isLocked: false, isLocked: false,
chartType: 'gc' chartType: 'mer'
}; };
this.foodSources.push(gc); this.foodSources.push(fred);
this.renderFoodSource(gc); this.renderFoodSource(fred);
this.gcRefId = gc.id; this.fredRefId = fred.id;
const kibble = { const kibble = {
id: this.generateFoodSourceId(), id: this.generateFoodSourceId(),
@ -267,7 +277,7 @@
this.foodSources.splice(index, 1); this.foodSources.splice(index, 1);
// If reference IDs were removed, clear them // If reference IDs were removed, clear them
if (this.kibbleRefId === id) this.kibbleRefId = null; 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 // Remove the DOM element
const element = document.getElementById(`foodSource-${id}`); const element = document.getElementById(`foodSource-${id}`);
@ -678,7 +688,7 @@
const container = document.getElementById('foodSources'); const container = document.getElementById('foodSources');
if (!container) return; if (!container) return;
const isChart = foodSource.chartType === 'gc' || foodSource.chartType === 'kibble'; const isChart = foodSource.chartType === 'kibble';
const energyReadonlyAttr = isChart ? 'readonly' : ''; const energyReadonlyAttr = isChart ? 'readonly' : '';
const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content'; const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content';
const unitDisabledAttr = isChart ? 'disabled' : ''; const unitDisabledAttr = isChart ? 'disabled' : '';
@ -969,8 +979,8 @@
const addFoodBtn = document.getElementById('addFoodBtn'); const addFoodBtn = document.getElementById('addFoodBtn');
if (weightInput) { if (weightInput) {
weightInput.addEventListener('input', () => this.updateCalorieCalculations()); weightInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
weightInput.addEventListener('blur', () => this.validateWeight()); weightInput.addEventListener('blur', () => { this.validateWeight(); this.saveStateToStorage(); });
} }
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations()); if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
@ -1335,26 +1345,38 @@
return lowerVal + (upperVal - lowerVal) * t; return lowerVal + (upperVal - lowerVal) * t;
} }
// Kaya: GC chart interpolation across buckets // Kaya: MER-based grams/day for Fred & Felia (kibble remains chart-based)
getGCChartGramsForAge(ageMonths) { getKayaMerFactorForAge(ageMonths) {
// Buckets per guideline with boundary rule (5.0 → later bucket, 7.0 → later bucket) if (ageMonths === null || ageMonths === undefined) return null;
// <5 mo (2.0<5.0): 950→1350 if (ageMonths < 4) return CALCULATOR_CONFIG.kayaMerFactorUnder4Months;
// 56 mo (5.06.0): 1250→1550; (6.0<7.0): hold 1550 return CALCULATOR_CONFIG.kayaMerFactorFrom4Months;
// 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);
} }
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(`energy-error-${fs.id}`, false);
}); });
this.showError('daysError', false); this.showError('daysError', false);
this.showError('weightError', false);
// Validate days input // Validate days input
if (!days || !this.validateInput(days, 1, true)) { 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) // Calculate per-food breakdown (chart-first)
const foodBreakdowns = []; const foodBreakdowns = [];
let totalDailyGrams = 0; let totalDailyGrams = 0;
@ -1453,8 +1486,8 @@
this.foodSources.forEach(fs => { this.foodSources.forEach(fs => {
const energyPer100g = this.getFoodSourceEnergyPer100g(fs); const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
let chartGrams = null; let chartGrams = null;
if (fs.chartType === 'gc') { if (fs.chartType === 'mer') {
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : null; chartGrams = age !== null ? this.getKayaMerBasedGramsForFood(age, energyPer100g) : null;
} else if (fs.chartType === 'kibble') { } else if (fs.chartType === 'kibble') {
chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null; chartGrams = age !== null ? this.getKayaKibbleGramsForAge(age) : null;
} }
@ -1474,7 +1507,7 @@
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => { firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
let dailyGramsForThisFood = 0; let dailyGramsForThisFood = 0;
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0); let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
if ((fs.chartType === 'gc' || fs.chartType === 'kibble')) { if ((fs.chartType === 'mer' || fs.chartType === 'kibble')) {
if (gramsPortion !== null) { if (gramsPortion !== null) {
dailyGramsForThisFood = gramsPortion; dailyGramsForThisFood = gramsPortion;
} }

View File

@ -7,5 +7,11 @@ const CALCULATOR_CONFIG = {
defaultScale: 1.0, defaultScale: 1.0,
maxFoodSources: 5, maxFoodSources: 5,
minScale: 0.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
}; };