8 Commits

Author SHA1 Message Date
Dayowe 68d5527a89 fix(kaya): lock Fred & Felia kcal/100g (always 115) 2026-02-06 12:52:13 +01:00
Dayowe 19592f2230 • 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/
2026-01-28 15:51:28 +01:00
Dayowe d0a30a8f8d Add local stoarge 2025-11-12 18:39:10 +01:00
Dayowe 374d067cf4 Fixes and improvements 2025-11-12 18:34:26 +01:00
Dayowe 73c4648978 Updates 2025-11-12 18:00:36 +01:00
Dayowe da9fd20ffb Remve oz, cups, lb 2025-11-12 17:00:45 +01:00
Dayowe d489a87722 Add doc 2025-11-12 16:48:43 +01:00
Dayowe 7fd139b321 Kaya transition v1 2025-11-12 16:44:43 +01:00
8 changed files with 1210 additions and 753 deletions
+43
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
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.
+190
View File
@@ -0,0 +1,190 @@
# Feeding Transition Calculator — Implementation Guide (30 kg adult)
## 1) Purpose & principle
- The calculator **keeps energy intake continuous** while transitioning from kibble (current) to gently cooked (GC, target).
- Internally it uses the **kibble charts month-level precision**; externally it **communicates in GC phases** (<5 mo, 56 mo, 712 mo).
* * *
## 2) Fixed scope
- Dog profile: **30 kg adult** only.
- Supported ages: **2.0 to 12.0 months** inclusive.
If the user enters a value outside this range, **clamp** to the nearest bound and show a subtle note.
- Required configuration at runtime: **kibble energy density (kcal per 100 g)**.
If missing, show an error and do not compute.
* * *
## 3) Data you must hard-code
1. **Kibble reference points (grams/day, 30 kg column):**
(2→250), (3→330), (4→365), (6→400), (8→410), (10→410), (12→405).
2. **Interpolated monthly kibble (round to whole grams):**
- 2 mo: 250
- 3 mo: 330
- 4 mo: 365
- 5 mo: 382
- 6 mo: 400
- 7 mo: 405
- 8 mo: 410
- 9 mo: 410
- 10 mo: 410
- 11 mo: 408
- 12 mo: 405
3. **GC communication buckets and ranges (g/day):**
- **< 5 months** (covers 2.04.999… mo): **9501350**
- **56 months** (covers 5.06.999… mo): **12501550**
- **712 months** (covers 7.012.0 mo): **13001500**
**Boundary rule:** exact 5.0 and 7.0 belong to the **later** bucket (5.0 → “56”, 7.0 → “712”).
* * *
## 4) How to calculate results (conceptual, no code)
### A) Kibble grams/day at any age (2.012.0)
- Use **linear interpolation** between the nearest kibble reference points listed above.
- Round the resulting kibble grams/day to **whole grams** (or to the nearest **5 g** if the user enables a rounding toggle).
### B) GC bucket assignment
- Based on the **age**, assign the corresponding GC bucket and attach that buckets **low/high range** (g/day).
### C) Energy-matched GC grams/day (the backbone)
- Convert kibble grams/day to **kcal/day** using the **user-provided** kibble energy density (kcal/100 g).
- Convert kcal/day to **GC grams/day** using GCs energy density **115 kcal per 100 g**.
(Equivalently, GC provides **1.15 kcal per gram**.)
- Round to **whole grams** (or to **nearest 5 g** if the user toggled it).
### D) Range status
- Compare the **energy-matched GC grams/day** to the GC buckets **low/high**:
- “within” if inside \[low, high\]
- “below” if under the low
- “above” if over the high
(Do **not** alter the energy-matched amount; just flag it.)
### E) Transition schedule (blended days)
- Default to **7 days** (configurable).
- Linearly ramp daily fractions from **100% kibble / 0% GC** on Day 1 to **0% kibble / 100% GC** on the last day, in equal steps.
- Each days grams = (kibble grams/day × kibble fraction) + (GC grams/day × GC fraction).
Round after multiplying (whole grams or nearest 5 g per the user setting).
* * *
## 5) What to display (UX rules)
1. **Primary number:** “Gently cooked (energy-matched): **X g/day**”.
Directly below, show the GC bucket label and its range (e.g., “712 months: 13001500 g/day”) plus a small **status chip** (within/below/above).
2. **Context line:** “Based on your kibble energy density: **Y kcal / 100 g**” with an edit control.
3. **Age input:** accept decimals (e.g., 5.5 months).
Add tick marks at 2, 3, 4, 6, 8, 10, 12 (the original kibble points).
4. **Transition widget:** a simple 57 day table or bar chart that shows **kibble g** and **GC g** per day, plus the day total.
(Totals will typically increase during the transition because GC is less energy-dense; this is expected.)
5. **Rounding toggle:** whole grams vs nearest 5 g.
6. **Download/export:** CSV with columns:
age_months, kibble_g_per_day, kibble_kcal_per_100g, kcal_per_day, gc_energy_matched_g_per_day, gc_bucket_name, gc_low_g, gc_high_g, range_status, and per-day transition grams.
* * *
## 6) Validation & edge cases
- **Missing kibble kcal density:** block calculation and display a clear prompt to enter kcal/100 g.
- **Age outside 212 months:** clamp to the nearest bound; show a subtle informational note.
- **Energy-matched GC outside bucket range:** keep the energy-matched number; display the status chip and a short educational tooltip (growth varies; this tool prioritizes energy continuity).
- **Rounding:** perform all math in floating point; **round only for display** (and for the per-day plan after multiplying by fractions).
* * *
## 7) Acceptance checks (use these to verify)
- At **5.5 months** with **380 kcal/100 g** kibble:
- Interpolated kibble ≈ **391 g/day**.
- Kcal/day ≈ **1,486 kcal**.
- Energy-matched GC ≈ **1,292 g/day**.
- GC bucket “56 months” (1,2501,550) → **within**.
- At **8.0 months** with **380 kcal/100 g** kibble:
- Kibble ≈ **410 g/day** → ≈ **1,558 kcal/day** → GC ≈ **1,355 g/day**.
- GC bucket “712 months” (1,3001,500) → **within**.
- At **2.0 months** with **380 kcal/100 g** kibble:
- Kibble **250 g/day****950 kcal/day** → GC **≈ 826 g/day**.
- GC bucket “<5 months” (9501,350) → **below** (expected for some formulas).
* * *
## 8) Deliverables checklist
- Precise monthly interpolation (ready values above).
- Age-aware GC bucket labelling and ranges.
- Energy-matched GC grams/day with status flag.
- Configurable transition length; per-day blend table.
- Rounding control.
+465 -372
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -190,6 +190,14 @@
color: var(--text-label);
}
/* Kaya end-weight readonly field: compact, non-editable */
#kayaEndWeight {
width: 120px;
display: inline-block;
}
.dog-calculator-results {
background: linear-gradient(135deg, rgba(241, 154, 95, 0.08) 0%, rgba(241, 154, 95, 0.04) 100%);
border: 1px solid rgba(241, 154, 95, 0.2);
+11 -75
View File
@@ -1,50 +1,26 @@
<div class="dog-calculator-container" id="dogCalculator">
<div class="dog-calculator-section">
<div class="dog-calculator-section-header">
<h2>Dog's Characteristics</h2>
<div class="dog-calculator-unit-switch">
<span class="dog-calculator-unit-label active" id="metricLabel">Metric</span>
<label class="dog-calculator-switch">
<input type="checkbox" id="unitToggle">
<span class="dog-calculator-slider"></span>
</label>
<span class="dog-calculator-unit-label" id="imperialLabel">Imperial</span>
</div>
<h2>Kayas Transition</h2>
</div>
<div class="dog-calculator-form-group">
<label for="dogType">Dog Type / Activity Level:</label>
<select id="dogType" aria-describedby="dogTypeHelp">
<option value="">Select dog type...</option>
<option value="3.0">Puppy (0-4 months)</option>
<option value="2.0">Puppy (4 months - adult)</option>
<option value="1.2">Adult - inactive/obese</option>
<option value="1.6">Adult (neutered/spayed) - average activity</option>
<option value="1.8">Adult (intact) - average activity</option>
<option value="1.0">Adult - weight loss</option>
<option value="1.7">Adult - weight gain</option>
<option value="2.0">Working dog - light work</option>
<option value="3.0">Working dog - moderate work</option>
<option value="5.0">Working dog - heavy work</option>
<option value="1.1">Senior dog</option>
</select>
<label for="ageMonths">Kayas age (months):</label>
<input type="number" id="ageMonths" min="2" max="12" step="0.1" placeholder="Enter age in months" aria-describedby="ageHelp">
<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">Dog's Weight (kg):</label>
<input type="number" id="weight" min="0.1" step="0.1" placeholder="Enter weight in kg" aria-describedby="weightHelp">
<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-results" id="calorieResults" style="display: none;">
<div class="dog-calculator-result-item">
<span class="dog-calculator-result-label">Resting Energy Requirement (RER):</span>
<span class="dog-calculator-result-value" id="rerValue">- cal/day</span>
</div>
<div class="dog-calculator-result-item">
<span class="dog-calculator-result-label">Maintenance Energy Requirement (MER):</span>
<span class="dog-calculator-result-value" id="merValue">- cal/day</span>
</div>
<div class="dog-calculator-form-group">
<label for="kayaEndWeight">Kayas endweight:</label>
<input type="text" id="kayaEndWeight" value="30 kg" readonly>
</div>
</div>
@@ -98,9 +74,6 @@
<div class="dog-calculator-unit-buttons" id="unitButtons" style="display: none;">
<button type="button" class="dog-calculator-unit-btn active" data-unit="g">g</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="kg">kg</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="oz">oz</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="lb">lb</button>
<button type="button" class="dog-calculator-unit-btn" data-unit="cups" id="cupsButton" disabled title="Available when using kcal/cup measurement">cups</button>
</div>
<!-- Daily Total Results -->
@@ -115,9 +88,6 @@
<select id="unit" class="dog-calculator-unit-select-hidden" aria-describedby="unitHelp">
<option value="g">grams (g)</option>
<option value="kg">kilograms (kg)</option>
<option value="oz">ounces (oz)</option>
<option value="lb">pounds (lb)</option>
<option value="cups">cups</option>
</select>
<div class="dog-calculator-food-amounts-section" id="foodAmountsSection" style="display: none;">
@@ -139,44 +109,10 @@
</div>
</div>
<div class="dog-calculator-action-buttons">
<button class="dog-calculator-btn dog-calculator-btn-share" id="shareBtn">
Share
</button>
</div>
<div class="dog-calculator-footer">
<a href="https://caninenutritionandwellness.com" target="_blank" rel="noopener noreferrer">
by caninenutritionandwellness.com
</a>
</div>
<!-- Share Modal -->
<div id="shareModal" class="dog-calculator-modal" style="display: none;">
<div class="dog-calculator-modal-content">
<span class="dog-calculator-modal-close" id="shareModalClose">&times;</span>
<h3>Share Calculator</h3>
<div class="dog-calculator-share-buttons">
<button class="dog-calculator-share-btn dog-calculator-share-facebook plausible-event-name=Calculator+Usage plausible-event-action=calculator-share-facebook" id="shareFacebook">
Facebook
</button>
<button class="dog-calculator-share-btn dog-calculator-share-twitter plausible-event-name=Calculator+Usage plausible-event-action=calculator-share-twitter" id="shareTwitter">
Twitter
</button>
<button class="dog-calculator-share-btn dog-calculator-share-linkedin plausible-event-name=Calculator+Usage plausible-event-action=calculator-share-linkedin" id="shareLinkedIn">
LinkedIn
</button>
<button class="dog-calculator-share-btn dog-calculator-share-email plausible-event-name=Calculator+Usage plausible-event-action=calculator-share-email" id="shareEmail">
Email
</button>
<button class="dog-calculator-share-btn dog-calculator-share-copy plausible-event-name=Calculator+Usage plausible-event-action=calculator-share-copy-link" id="shareCopy">
Copy Link
</button>
</div>
<div class="dog-calculator-share-url">
<input type="text" id="shareUrl" readonly>
</div>
</div>
</div>
</div>
+439 -296
View File
@@ -15,15 +15,22 @@
this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources;
this.mealsPerDay = 2;
this.showPerMeal = false;
// Kayafied reference source tracking
this.kibbleRefId = null;
this.fredRefId = null;
this.storageKey = 'kaya_calculator_state_v1';
this.init();
}
init() {
this.applyTheme();
this.applyScale();
this.initializeFoodSources();
this.bindEvents();
this.updateUnitLabels();
const restored = this.loadStateFromStorage();
if (!restored) {
this.initializeFoodSources();
}
this.bindEvents();
this.setupIframeResize();
// Show the calculator with fade-in
@@ -31,6 +38,129 @@
container.classList.add('loaded');
}
// Persistence helpers
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,
mealsPerDay: this.mealsPerDay,
foodSources: this.foodSources.map(fs => ({
id: fs.id,
name: fs.name,
energy: fs.energy,
energyUnit: fs.energyUnit,
percentage: fs.percentage,
isLocked: fs.isLocked,
chartType: fs.chartType || null,
splitByMeals: (fs.splitByMeals === undefined ? true : fs.splitByMeals)
}))
};
localStorage.setItem(this.storageKey, JSON.stringify(state));
} catch (e) {
// Ignore storage errors (private mode, etc.)
}
}
loadStateFromStorage() {
try {
const raw = localStorage.getItem(this.storageKey);
if (!raw) return false;
const state = JSON.parse(raw);
if (!state || typeof state !== 'object') return false;
// Restore unit first
const unitSelect = document.getElementById('unit');
if (unitSelect && state.unit && (state.unit === 'g' || state.unit === 'kg')) {
unitSelect.value = state.unit;
this.setActiveUnitButton(state.unit);
}
// Restore days
const daysInput = document.getElementById('days');
if (daysInput && state.days) {
daysInput.value = state.days;
this.updateDayLabel();
}
// Restore meal settings
this.showPerMeal = !!state.showPerMeal;
this.mealsPerDay = state.mealsPerDay || 2;
const showDaily = document.getElementById('showDaily');
const showPerMeal = document.getElementById('showPerMeal');
const mealsPerDayInput = document.getElementById('mealsPerDay');
const mealInputGroup = document.getElementById('mealInputGroup');
if (showDaily && showPerMeal) {
if (this.showPerMeal) {
showPerMeal.checked = true;
if (mealInputGroup) mealInputGroup.style.display = 'inline-flex';
} else {
showDaily.checked = true;
if (mealInputGroup) mealInputGroup.style.display = 'none';
}
}
if (mealsPerDayInput) {
mealsPerDayInput.value = this.mealsPerDay;
}
// Restore age
const ageInput = document.getElementById('ageMonths');
if (ageInput && (state.age || state.age === 0)) {
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.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 normalizedEnergy = chartType === 'mer' ? '115' : (saved.energy || '');
const normalizedEnergyUnit = chartType === 'mer' ? 'kcal100g' : (saved.energyUnit || 'kcal100g');
const fs = {
id: saved.id || this.generateFoodSourceId(),
name: saved.name || 'Food Source',
energy: normalizedEnergy,
energyUnit: normalizedEnergyUnit,
percentage: typeof saved.percentage === 'number' ? saved.percentage : 0,
isLocked: !!saved.isLocked,
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 === 'mer' && !this.fredRefId) this.fredRefId = fs.id;
});
this.updateAddButton();
this.updateRemoveButtons();
this.refreshAllPercentageUI();
}
// Trigger calculations
this.updateCalorieCalculations();
return true;
} catch (e) {
return false;
}
}
getThemeFromURL() {
const urlParams = new URLSearchParams(window.location.search);
const theme = urlParams.get('theme');
@@ -69,8 +199,49 @@
// Food Source Management Methods
initializeFoodSources() {
this.addFoodSource();
// Seed three sources for Kaya's transition
const fred = {
id: this.generateFoodSourceId(),
name: 'Fred & Felia (Junior Huhn)',
energy: '115',
energyUnit: 'kcal100g',
percentage: 5,
isLocked: false,
chartType: 'mer'
};
this.foodSources.push(fred);
this.renderFoodSource(fred);
this.fredRefId = fred.id;
const kibble = {
id: this.generateFoodSourceId(),
name: 'Eukanuba (Large Breed Fresh Chicken)',
energy: '372',
energyUnit: 'kcal100g',
percentage: 95,
isLocked: false,
chartType: 'kibble'
};
this.foodSources.push(kibble);
this.renderFoodSource(kibble);
this.kibbleRefId = kibble.id;
const treats = {
id: this.generateFoodSourceId(),
name: 'Treats',
energy: '',
energyUnit: 'kcal100g',
percentage: 0,
isLocked: false,
chartType: null,
splitByMeals: false
};
this.foodSources.push(treats);
this.renderFoodSource(treats);
this.updateAddButton();
this.updateRemoveButtons();
this.refreshAllPercentageUI();
}
addFoodSource() {
@@ -94,6 +265,7 @@
this.updateAddButton();
this.updateRemoveButtons();
this.refreshAllPercentageUI();
this.saveStateToStorage();
}
removeFoodSource(id) {
@@ -105,6 +277,9 @@
if (index === -1) return;
this.foodSources.splice(index, 1);
// If reference IDs were removed, clear them
if (this.kibbleRefId === id) this.kibbleRefId = null;
if (this.fredRefId === id) this.fredRefId = null;
// Remove the DOM element
const element = document.getElementById(`foodSource-${id}`);
@@ -118,6 +293,8 @@
this.updateAddButton();
this.updateRemoveButtons();
this.refreshAllPercentageUI();
this.updateCalorieCalculations();
this.saveStateToStorage();
}
generateFoodSourceId() {
@@ -399,6 +576,7 @@
}
});
this.saveStateToStorage();
return true;
}
@@ -419,6 +597,7 @@
// Update food calculations
this.updateFoodCalculations();
this.saveStateToStorage();
}
updateSliderConstraints(foodSource) {
@@ -511,6 +690,11 @@
const container = document.getElementById('foodSources');
if (!container) return;
const isChart = foodSource.chartType === 'kibble' || foodSource.chartType === 'mer';
const energyReadonlyAttr = isChart ? 'readonly' : '';
const energyTitle = isChart ? 'Chart-based food: kcal locked' : 'Enter energy content';
const unitDisabledAttr = isChart ? 'disabled' : '';
const cardHTML = `
<div class="dog-calculator-food-source-card" id="foodSource-${foodSource.id}">
<div class="dog-calculator-food-source-header">
@@ -521,11 +705,11 @@
<div class="dog-calculator-input-group">
<div class="dog-calculator-form-group">
<label for="energy-${foodSource.id}">Energy Content:</label>
<input type="number" id="energy-${foodSource.id}" min="1" step="1" placeholder="Enter energy content" value="${foodSource.energy}">
<input type="number" id="energy-${foodSource.id}" ${energyReadonlyAttr} title="${energyTitle}" min="1" step="1" placeholder="Enter energy content" value="${foodSource.energy}">
</div>
<div class="dog-calculator-form-group">
<label for="energy-unit-${foodSource.id}">Unit:</label>
<select id="energy-unit-${foodSource.id}" class="dog-calculator-unit-select">
<select id="energy-unit-${foodSource.id}" class="dog-calculator-unit-select" ${unitDisabledAttr} title="${isChart ? 'Chart-based food: unit locked' : 'Select energy unit'}">
<option value="kcal100g" ${foodSource.energyUnit === 'kcal100g' ? 'selected' : ''}>kcal/100g</option>
<option value="kcalkg" ${foodSource.energyUnit === 'kcalkg' ? 'selected' : ''}>kcal/kg</option>
<option value="kcalcup" ${foodSource.energyUnit === 'kcalcup' ? 'selected' : ''}>kcal/cup</option>
@@ -573,6 +757,7 @@
const newName = nameInput.value.trim() || `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`;
this.updateFoodSourceData(id, 'name', newName);
this.updateFoodCalculations(); // This will refresh the food amount breakdown with new names
this.saveStateToStorage();
});
nameInput.addEventListener('blur', () => {
@@ -582,50 +767,42 @@
nameInput.value = defaultName;
this.updateFoodSourceData(id, 'name', defaultName);
this.updateFoodCalculations();
this.saveStateToStorage();
}
});
}
if (energyInput) {
if (energyInput && !energyInput.hasAttribute('readonly')) {
energyInput.addEventListener('input', () => {
this.updateFoodSourceData(id, 'energy', energyInput.value);
// If kibble reference changed, recompute daily target
if (id === this.kibbleRefId) {
this.updateCalorieCalculations();
}
// Auto-select cups when entering energy for kcal/cup
const foodSource = this.foodSources.find(fs => fs.id === id);
if (foodSource && foodSource.energyUnit === 'kcalcup' && parseFloat(energyInput.value) > 0) {
// Cups display removed; default to grams
const unitSelect = document.getElementById('unit');
const cupsButton = document.getElementById('cupsButton');
// First check if cups button will be enabled after update
const willEnableCups = this.foodSources.some(fs =>
fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0
);
if (willEnableCups && unitSelect) {
// Set cups BEFORE updating calculations
unitSelect.value = 'cups';
unitSelect.setAttribute('value', 'cups');
this.setActiveUnitButton('cups');
// Enable the cups button manually since we know it will be valid
if (cupsButton) {
cupsButton.disabled = false;
cupsButton.title = 'Show amounts in cups';
}
if (unitSelect) {
unitSelect.value = 'g';
unitSelect.setAttribute('value', 'g');
this.setActiveUnitButton('g');
}
// Now update calculations with cups already selected
this.updateFoodCalculations();
} else {
this.updateFoodCalculations();
}
this.updateFoodCalculations();
this.saveStateToStorage();
});
energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id));
}
if (energyUnitSelect) {
if (energyUnitSelect && !energyUnitSelect.hasAttribute('disabled')) {
energyUnitSelect.addEventListener('change', () => {
this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value);
if (id === this.kibbleRefId) {
this.updateCalorieCalculations();
}
// Auto-select the most appropriate unit based on energy unit
const unitSelect = document.getElementById('unit');
@@ -634,47 +811,40 @@
if (unitSelect) {
switch(energyUnitSelect.value) {
case 'kcalcup':
// Check if we have energy value to enable cups
const foodSource = this.foodSources.find(fs => fs.id === id);
if (foodSource && foodSource.energy && parseFloat(foodSource.energy) > 0) {
// Set cups BEFORE updating calculations
unitSelect.value = 'cups';
unitSelect.setAttribute('value', 'cups');
this.setActiveUnitButton('cups');
// Enable the cups button manually
const cupsButton = document.getElementById('cupsButton');
if (cupsButton) {
cupsButton.disabled = false;
cupsButton.title = 'Show amounts in cups';
}
}
this.updateFoodCalculations();
break;
case 'kcal100g':
// For kcal/100g, select grams
// Cups display not available; default to grams
unitSelect.value = 'g';
this.setActiveUnitButton('g');
this.updateFoodCalculations();
break;
case 'kcalkg':
// For kcal/kg, also select grams (or could be kg)
unitSelect.value = 'g';
this.setActiveUnitButton('g');
this.updateFoodCalculations();
break;
case 'kcalcan':
// For kcal/can, use grams as default (or ounces in imperial)
unitSelect.value = this.isImperial ? 'oz' : 'g';
this.setActiveUnitButton(unitSelect.value);
this.updateFoodCalculations();
break;
}
} else {
// No unit select, just update calculations
this.updateFoodCalculations();
this.saveStateToStorage();
break;
case 'kcal100g':
// For kcal/100g, select grams
unitSelect.value = 'g';
this.setActiveUnitButton('g');
this.updateFoodCalculations();
this.saveStateToStorage();
break;
case 'kcalkg':
// For kcal/kg, also select grams (or could be kg)
unitSelect.value = 'g';
this.setActiveUnitButton('g');
this.updateFoodCalculations();
this.saveStateToStorage();
break;
case 'kcalcan':
// For kcal/can, use grams as default
unitSelect.value = 'g';
this.setActiveUnitButton('g');
this.updateFoodCalculations();
this.saveStateToStorage();
break;
}
});
} else {
// No unit select, just update calculations
this.updateFoodCalculations();
this.saveStateToStorage();
}
});
}
if (percentageSlider) {
@@ -728,6 +898,7 @@
this.updateLockIcon(id);
this.updateLockStates();
this.refreshAllPercentageUI();
this.saveStateToStorage();
}
updateLockIcon(id) {
@@ -803,27 +974,35 @@
bindEvents() {
const weightInput = document.getElementById('weight');
const dogTypeSelect = document.getElementById('dogType');
const ageInput = document.getElementById('ageMonths');
const daysInput = document.getElementById('days');
const unitSelect = document.getElementById('unit');
const unitToggle = document.getElementById('unitToggle');
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());
// Kayafied: age input drives energy target
if (ageInput) {
ageInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
ageInput.addEventListener('blur', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
}
if (daysInput) {
daysInput.addEventListener('input', () => {
this.updateDayLabel();
this.updateFoodCalculations();
this.saveStateToStorage();
});
daysInput.addEventListener('blur', () => this.validateDays());
}
if (unitSelect) unitSelect.addEventListener('change', () => this.updateFoodCalculations());
if (unitSelect) unitSelect.addEventListener('change', () => { this.updateFoodCalculations(); this.saveStateToStorage(); });
// Unit button event listeners
const unitButtons = document.querySelectorAll('.dog-calculator-unit-btn');
@@ -835,6 +1014,7 @@
if (unitSelect) {
unitSelect.value = selectedUnit;
this.updateFoodCalculations();
this.saveStateToStorage();
}
});
});
@@ -950,58 +1130,14 @@
imperialLabel.classList.toggle('active', this.isImperial);
}
if (this.isImperial) {
if (weightLabel) weightLabel.textContent = "Dog's Weight (lbs):";
if (weightInput) {
weightInput.placeholder = "Enter weight in lbs";
weightInput.min = "0.2";
weightInput.step = "0.1";
// Kaya: restrict to metric g/kg only
if (unitSelect) {
unitSelect.innerHTML = '<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>';
if (!unitSelect.value || (unitSelect.value !== 'g' && unitSelect.value !== 'kg')) {
unitSelect.value = 'g';
this.setActiveUnitButton('g');
}
if (unitSelect) {
unitSelect.innerHTML = '<option value="oz">ounces (oz)</option>' +
'<option value="lb">pounds (lb)</option>' +
'<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>';
unitSelect.value = 'oz'; // Auto-select ounces for imperial
this.setActiveUnitButton('oz'); // Sync unit buttons
}
// Update energy units for all food sources to kcal/cup for imperial
this.foodSources.forEach(fs => {
if (fs.energyUnit === 'kcal100g') {
fs.energyUnit = 'kcalcup';
const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`);
if (energyUnitSelect) {
energyUnitSelect.value = 'kcalcup';
}
}
});
} else {
if (weightLabel) weightLabel.textContent = "Dog's Weight (kg):";
if (weightInput) {
weightInput.placeholder = "Enter weight in kg";
weightInput.min = "0.1";
weightInput.step = "0.1";
}
if (unitSelect) {
unitSelect.innerHTML = '<option value="g">grams (g)</option>' +
'<option value="kg">kilograms (kg)</option>' +
'<option value="oz">ounces (oz)</option>' +
'<option value="lb">pounds (lb)</option>';
unitSelect.value = 'g'; // Auto-select grams for metric
this.setActiveUnitButton('g'); // Sync unit buttons
}
// Update energy units for all food sources to kcal/100g for metric
this.foodSources.forEach(fs => {
if (fs.energyUnit === 'kcalcup') {
fs.energyUnit = 'kcal100g';
const energyUnitSelect = document.getElementById(`energy-unit-${fs.id}`);
if (energyUnitSelect) {
energyUnitSelect.value = 'kcal100g';
}
}
});
}
}
@@ -1158,87 +1294,103 @@
}
updateCalorieCalculations() {
const dogTypeSelect = document.getElementById('dogType');
const calorieResults = document.getElementById('calorieResults');
const rerValue = document.getElementById('rerValue');
const merValue = document.getElementById('merValue');
// Kaya-specific: only track age and trigger recompute
const ageInput = document.getElementById('ageMonths');
const ageClampNote = document.getElementById('ageClampNote');
if (!dogTypeSelect || !calorieResults || !rerValue || !merValue) {
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
if (!ageInput || ageInput.value === '') {
this.currentAge = null;
this.updateFoodCalculations();
return;
}
const weightKg = this.getWeightInKg();
const dogTypeFactor = dogTypeSelect.value;
this.showError('weightError', false);
if (!weightKg || weightKg < 0.1) {
const weightInput = document.getElementById('weight');
if (weightInput && weightInput.value) this.showError('weightError', true);
calorieResults.style.display = 'none';
let age = parseFloat(ageInput.value);
if (isNaN(age)) {
this.currentAge = null;
this.updateFoodCalculations();
return;
}
if (!dogTypeFactor) {
calorieResults.style.display = 'none';
return;
}
const factor = parseFloat(dogTypeFactor);
const rer = this.calculateRER(weightKg);
const mer = this.calculateMER(rer, factor);
// Calculate range for MER
const range = this.getLifeStageRange(factor);
this.currentMERMin = this.calculateMER(rer, range.min);
this.currentMERMax = this.calculateMER(rer, range.max);
this.currentMER = mer; // Keep middle/selected value for compatibility
rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day';
// Show MER as range if applicable
if (range.min !== range.max) {
merValue.textContent = this.formatNumber(this.currentMERMin, 0) + '-' +
this.formatNumber(this.currentMERMax, 0) + ' cal/day';
} else {
merValue.textContent = this.formatNumber(mer, 0) + ' cal/day';
}
calorieResults.style.display = 'block';
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
this.currentAge = age;
this.updateFoodCalculations();
}
// Kaya: monthly table with interpolation between months
getKayaKibbleGramsForAge(ageMonths) {
// Precomputed monthly values for 30 kg (g/day)
const table = {
2: 250,
3: 330,
4: 365,
5: 382,
6: 400,
7: 405,
8: 410,
9: 410,
10: 410,
11: 408,
12: 405
};
// Exact integer month
const lower = Math.floor(ageMonths);
const upper = Math.ceil(ageMonths);
if (lower === upper) return table[lower] || 0;
// Linear interpolation between bounds
const lowerVal = table[lower] || 0;
const upperVal = table[upper] || 0;
const t = (ageMonths - lower) / (upper - lower);
return lowerVal + (upperVal - lowerVal) * t;
}
// 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;
}
updateCupsButtonState() {
const cupsButton = document.getElementById('cupsButton');
if (!cupsButton) return;
// Check if any food source has kcal/cup selected
const hasKcalCup = this.foodSources.some(fs =>
fs.energyUnit === 'kcalcup' && fs.energy && parseFloat(fs.energy) > 0
);
if (hasKcalCup) {
cupsButton.disabled = false;
cupsButton.title = 'Show amounts in cups';
} else {
cupsButton.disabled = true;
cupsButton.title = 'Available when using kcal/cup measurement';
// If cups was selected, switch back to grams
const unitSelect = document.getElementById('unit');
if (unitSelect && unitSelect.value === 'cups') {
unitSelect.value = 'g';
this.setActiveUnitButton('g');
}
}
// Cups UI is not used in this configuration
return;
}
updateFoodCalculations() {
if (this.currentMER === 0) return;
// Check if we have a range
const hasRange = this.currentMERMin !== this.currentMERMax;
// Chart-first: no MER check
const hasRange = false;
const daysInput = document.getElementById('days');
const unitSelect = document.getElementById('unit');
@@ -1277,7 +1429,7 @@
// Debug: log what unit is being used
console.log('UpdateFoodCalculations - unit:', unit, 'unitLabel:', unitLabel);
// Determine frequency suffix for display
// Determine frequency suffix for display (will adjust per-item below)
const frequencySuffix = this.showPerMeal ? '/meal' : '/day';
// Clear all food source errors first
@@ -1285,6 +1437,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)) {
@@ -1302,101 +1455,95 @@
const numDays = parseInt(days);
// Calculate per-food breakdown
// Require a valid age for chart-first outputs
const ageInput = document.getElementById('ageMonths');
let age = ageInput && ageInput.value !== '' ? parseFloat(ageInput.value) : null;
if (age !== null) {
if (isNaN(age)) age = null;
if (age !== null) {
if (age < 2) age = 2;
if (age > 12) age = 12;
}
}
// 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;
let hasValidFoods = false;
// First pass: charted baseline
let chartedKcal = 0;
let chartedPercent = 0;
const firstPass = [];
this.foodSources.forEach(fs => {
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
const dailyCaloriesForThisFood = (this.currentMER * fs.percentage) / 100;
// Calculate range values if applicable
const dailyCaloriesMin = hasRange ? (this.currentMERMin * fs.percentage) / 100 : dailyCaloriesForThisFood;
const dailyCaloriesMax = hasRange ? (this.currentMERMax * fs.percentage) / 100 : dailyCaloriesForThisFood;
let dailyGramsForThisFood;
let dailyGramsMin, dailyGramsMax;
let dailyCupsForThisFood = null;
let dailyCupsMin, dailyCupsMax;
// For kcal/cup, calculate cups directly from calories
if (fs.energyUnit === 'kcalcup' && fs.energy) {
const caloriesPerCup = parseFloat(fs.energy);
dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup;
dailyCupsMin = dailyCaloriesMin / caloriesPerCup;
dailyCupsMax = dailyCaloriesMax / caloriesPerCup;
// We still need grams for total calculation, use approximation
dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
} else {
// For other units, calculate grams normally
dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
}
// Calculate per-meal amounts if needed
const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood;
const displayGramsMin = this.showPerMeal ? dailyGramsMin / this.mealsPerDay : dailyGramsMin;
const displayGramsMax = this.showPerMeal ? dailyGramsMax / this.mealsPerDay : dailyGramsMax;
const displayCups = dailyCupsForThisFood !== null ?
(this.showPerMeal ? dailyCupsForThisFood / this.mealsPerDay : dailyCupsForThisFood) : null;
const displayCupsMin = dailyCupsMin !== undefined ?
(this.showPerMeal ? dailyCupsMin / this.mealsPerDay : dailyCupsMin) : null;
const displayCupsMax = dailyCupsMax !== undefined ?
(this.showPerMeal ? dailyCupsMax / this.mealsPerDay : dailyCupsMax) : null;
const displayCalories = this.showPerMeal ? dailyCaloriesForThisFood / this.mealsPerDay : dailyCaloriesForThisFood;
const displayCaloriesMin = this.showPerMeal ? dailyCaloriesMin / this.mealsPerDay : dailyCaloriesMin;
const displayCaloriesMax = this.showPerMeal ? dailyCaloriesMax / this.mealsPerDay : dailyCaloriesMax;
foodBreakdowns.push({
name: fs.name,
percentage: fs.percentage,
dailyGrams: dailyGramsForThisFood,
dailyGramsMin: dailyGramsMin,
dailyGramsMax: dailyGramsMax,
displayGrams: displayGrams,
displayGramsMin: displayGramsMin,
displayGramsMax: displayGramsMax,
dailyCups: dailyCupsForThisFood,
dailyCupsMin: dailyCupsMin,
dailyCupsMax: dailyCupsMax,
displayCups: displayCups,
displayCupsMin: displayCupsMin,
displayCupsMax: displayCupsMax,
calories: dailyCaloriesForThisFood,
displayCalories: displayCalories,
displayCaloriesMin: displayCaloriesMin,
displayCaloriesMax: displayCaloriesMax,
isLocked: fs.isLocked,
hasEnergyContent: true,
hasRange: hasRange,
foodSource: fs // Store reference for cups conversion
});
totalDailyGrams += dailyGramsForThisFood;
hasValidFoods = true;
} else if (fs.percentage > 0) {
// Include food sources without energy content but show them as needing energy content
foodBreakdowns.push({
name: fs.name,
percentage: fs.percentage,
dailyGrams: 0,
displayGrams: 0,
dailyCups: null,
displayCups: null,
calories: 0,
displayCalories: 0,
isLocked: fs.isLocked,
hasEnergyContent: false,
foodSource: fs // Store reference for cups conversion
});
let chartGrams = 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;
}
const gramsPortion = (chartGrams !== null && chartGrams !== undefined) ? (chartGrams * (fs.percentage || 0) / 100) : null;
if (gramsPortion !== null && energyPer100g && energyPer100g > 0) {
chartedKcal += gramsPortion * (energyPer100g / 100);
chartedPercent += (fs.percentage || 0);
}
firstPass.push({ fs, energyPer100g, gramsPortion });
});
const kcalPerPercent = chartedPercent > 0 ? (chartedKcal / chartedPercent) : null;
// Second pass: finalize amounts
let splitDailyTotal = 0;
let dailyOnlyTotal = 0;
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
let dailyGramsForThisFood = 0;
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
if ((fs.chartType === 'mer' || fs.chartType === 'kibble')) {
if (gramsPortion !== null) {
dailyGramsForThisFood = gramsPortion;
}
} else {
if (hasEnergyContent && kcalPerPercent && (fs.percentage || 0) > 0) {
const perGramKcal = energyPer100g / 100;
const kcalForFood = (fs.percentage || 0) * kcalPerPercent;
dailyGramsForThisFood = kcalForFood / perGramKcal;
} else {
hasEnergyContent = false;
dailyGramsForThisFood = 0;
}
}
const isDailyOnly = fs.splitByMeals === false;
const displayGrams = (this.showPerMeal && !isDailyOnly) ? (dailyGramsForThisFood / this.mealsPerDay) : dailyGramsForThisFood;
foodBreakdowns.push({
name: fs.name,
percentage: fs.percentage,
dailyGrams: dailyGramsForThisFood,
isDailyOnly: isDailyOnly,
displayGrams: displayGrams,
dailyCups: null,
displayCups: null,
calories: hasEnergyContent ? (dailyGramsForThisFood * (energyPer100g / 100)) : 0,
displayCalories: hasEnergyContent ? (this.showPerMeal ? (dailyGramsForThisFood * (energyPer100g / 100)) / this.mealsPerDay : (dailyGramsForThisFood * (energyPer100g / 100))) : 0,
isLocked: fs.isLocked,
hasEnergyContent: hasEnergyContent,
foodSource: fs
});
totalDailyGrams += dailyGramsForThisFood;
if (isDailyOnly) dailyOnlyTotal += dailyGramsForThisFood; else splitDailyTotal += dailyGramsForThisFood;
if (dailyGramsForThisFood > 0) hasValidFoods = true;
});
if (!hasValidFoods) {
@@ -1416,12 +1563,13 @@
const unitButtons = document.getElementById('unitButtons');
if (unitButtons) unitButtons.style.display = 'none';
// If we have any food sources without energy content, still show the breakdown section
if (foodBreakdowns.length > 0) {
// If we have any foods with >0% but missing energy, show warnings only for those
const visibleBreakdownsMissing = foodBreakdowns.filter(b => b.percentage > 0);
if (visibleBreakdownsMissing.length > 0) {
// Show food amounts section with warnings for missing energy content
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
const foodAmountsHTML = foodBreakdowns.map(breakdown => {
const foodAmountsHTML = visibleBreakdownsMissing.map(breakdown => {
const lockIndicator = breakdown.isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '';
return `
@@ -1472,31 +1620,24 @@
const unitButtons = document.getElementById('unitButtons');
if (unitButtons) unitButtons.style.display = 'flex';
// Update per-food breakdown
if (foodBreakdownList && foodBreakdowns.length > 1) {
const breakdownHTML = foodBreakdowns.map(breakdown => {
// Update per-food breakdown (show only items with >0%)
const visibleBreakdowns = foodBreakdowns.filter(b => b.percentage > 0);
if (foodBreakdownList && visibleBreakdowns.length > 0) {
const breakdownHTML = visibleBreakdowns.map(breakdown => {
let valueContent;
// Choose per-item frequency suffix: daily-only items stay /day even in per-meal view
const itemSuffix = (this.showPerMeal && !breakdown.isDailyOnly) ? '/meal' : '/day';
if (breakdown.hasEnergyContent) {
if (unit === 'cups') {
// For cups, use the pre-calculated cups value if available
if (breakdown.displayCups !== null) {
if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) {
valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`;
} else {
valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`;
}
valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${itemSuffix}`;
} else {
valueContent = `<span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span>`;
}
} else {
// For other units (g, kg, oz, lb)
if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) {
const minConverted = this.convertUnits(breakdown.displayGramsMin, unit);
const maxConverted = this.convertUnits(breakdown.displayGramsMax, unit);
valueContent = `${this.formatNumber(minConverted, decimals)}-${this.formatNumber(maxConverted, decimals)} ${unitLabel}${frequencySuffix}`;
} else {
valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${frequencySuffix}`;
}
valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${itemSuffix}`;
}
} else {
valueContent = `<span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span>`;
@@ -1519,7 +1660,8 @@
// Generate individual food amount breakdown
// Update daily food value with correct units
const displayTotal = this.showPerMeal ? totalDailyGrams / this.mealsPerDay : totalDailyGrams;
// When per-meal view is enabled, split-only items divide by meals/day; daily-only items remain as daily totals
const displayTotal = this.showPerMeal ? (splitDailyTotal / this.mealsPerDay + dailyOnlyTotal) : (splitDailyTotal + dailyOnlyTotal);
let convertedTotal;
let totalDisplayText;
@@ -1586,7 +1728,8 @@
dailyFoodValue.textContent = totalDisplayText;
// Build HTML for individual food amounts
const foodAmountsHTML = foodBreakdowns.map(breakdown => {
const foodAmountsVisible = foodBreakdowns.filter(b => b.percentage > 0);
const foodAmountsHTML = foodAmountsVisible.map(breakdown => {
const lockIndicator = breakdown.isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '';
if (!breakdown.hasEnergyContent) {
+7 -1
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
};