Compare commits
6 Commits
90657f9aa4
...
d0a30a8f8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0a30a8f8d | ||
|
|
374d067cf4 | ||
|
|
73c4648978 | ||
|
|
da9fd20ffb | ||
|
|
d489a87722 | ||
|
|
7fd139b321 |
190
docs/kaya-transition.md
Normal file
190
docs/kaya-transition.md
Normal 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 chart’s month-level precision**; externally it **communicates in GC phases** (<5 mo, 5–6 mo, 7–12 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.0–4.999… mo): **950–1350**
|
||||
|
||||
- **5–6 months** (covers 5.0–6.999… mo): **1250–1550**
|
||||
|
||||
- **7–12 months** (covers 7.0–12.0 mo): **1300–1500**
|
||||
|
||||
|
||||
**Boundary rule:** exact 5.0 and 7.0 belong to the **later** bucket (5.0 → “5–6”, 7.0 → “7–12”).
|
||||
|
||||
* * *
|
||||
|
||||
## 4) How to calculate results (conceptual, no code)
|
||||
|
||||
### A) Kibble grams/day at any age (2.0–12.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 bucket’s **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 GC’s 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 bucket’s **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 day’s 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., “7–12 months: 1300–1500 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 5–7 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 2–12 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 “5–6 months” (1,250–1,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 “7–12 months” (1,300–1,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” (950–1,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.
|
||||
|
||||
|
||||
|
||||
794
iframe.html
794
iframe.html
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
|
||||
@ -1,50 +1,20 @@
|
||||
<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>Kaya’s Transition</h2>
|
||||
</div>
|
||||
|
||||
<div class="dog-calculator-form-group">
|
||||
<label for="ageMonths">Kaya’s 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 2–12 month range.</div>
|
||||
</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>
|
||||
</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">
|
||||
<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>
|
||||
<label for="kayaEndWeight">Kaya’s end‑weight:</label>
|
||||
<input type="text" id="kayaEndWeight" value="30 kg" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -98,9 +68,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 +82,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 +103,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">×</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>
|
||||
|
||||
@ -15,15 +15,22 @@
|
||||
this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources;
|
||||
this.mealsPerDay = 2;
|
||||
this.showPerMeal = false;
|
||||
// Kayafied reference source tracking
|
||||
this.kibbleRefId = null;
|
||||
this.gcRefId = 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,117 @@
|
||||
container.classList.add('loaded');
|
||||
}
|
||||
|
||||
// Persistence helpers
|
||||
saveStateToStorage() {
|
||||
try {
|
||||
const ageInput = document.getElementById('ageMonths');
|
||||
const unitSelect = document.getElementById('unit');
|
||||
const daysInput = document.getElementById('days');
|
||||
const state = {
|
||||
version: 1,
|
||||
age: ageInput && ageInput.value !== '' ? parseFloat(ageInput.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 food sources
|
||||
if (Array.isArray(state.foodSources) && state.foodSources.length) {
|
||||
this.foodSources = [];
|
||||
this.kibbleRefId = null;
|
||||
this.gcRefId = null;
|
||||
state.foodSources.forEach(saved => {
|
||||
const fs = {
|
||||
id: saved.id || this.generateFoodSourceId(),
|
||||
name: saved.name || 'Food Source',
|
||||
energy: saved.energy || '',
|
||||
energyUnit: saved.energyUnit || 'kcal100g',
|
||||
percentage: typeof saved.percentage === 'number' ? saved.percentage : 0,
|
||||
isLocked: !!saved.isLocked,
|
||||
chartType: saved.chartType || null,
|
||||
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;
|
||||
});
|
||||
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 +187,49 @@
|
||||
|
||||
// Food Source Management Methods
|
||||
initializeFoodSources() {
|
||||
this.addFoodSource();
|
||||
// Seed three sources for Kaya's transition
|
||||
const gc = {
|
||||
id: this.generateFoodSourceId(),
|
||||
name: 'Fred & Felia (Junior Huhn)',
|
||||
energy: '115',
|
||||
energyUnit: 'kcal100g',
|
||||
percentage: 5,
|
||||
isLocked: false,
|
||||
chartType: 'gc'
|
||||
};
|
||||
this.foodSources.push(gc);
|
||||
this.renderFoodSource(gc);
|
||||
this.gcRefId = gc.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 +253,7 @@
|
||||
this.updateAddButton();
|
||||
this.updateRemoveButtons();
|
||||
this.refreshAllPercentageUI();
|
||||
this.saveStateToStorage();
|
||||
}
|
||||
|
||||
removeFoodSource(id) {
|
||||
@ -105,6 +265,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.gcRefId === id) this.gcRefId = null;
|
||||
|
||||
// Remove the DOM element
|
||||
const element = document.getElementById(`foodSource-${id}`);
|
||||
@ -118,6 +281,8 @@
|
||||
this.updateAddButton();
|
||||
this.updateRemoveButtons();
|
||||
this.refreshAllPercentageUI();
|
||||
this.updateCalorieCalculations();
|
||||
this.saveStateToStorage();
|
||||
}
|
||||
|
||||
generateFoodSourceId() {
|
||||
@ -399,6 +564,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
this.saveStateToStorage();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -419,6 +585,7 @@
|
||||
|
||||
// Update food calculations
|
||||
this.updateFoodCalculations();
|
||||
this.saveStateToStorage();
|
||||
}
|
||||
|
||||
updateSliderConstraints(foodSource) {
|
||||
@ -511,6 +678,11 @@
|
||||
const container = document.getElementById('foodSources');
|
||||
if (!container) return;
|
||||
|
||||
const isChart = foodSource.chartType === 'gc' || foodSource.chartType === 'kibble';
|
||||
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 +693,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 +745,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 +755,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 +799,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 +886,7 @@
|
||||
this.updateLockIcon(id);
|
||||
this.updateLockStates();
|
||||
this.refreshAllPercentageUI();
|
||||
this.saveStateToStorage();
|
||||
}
|
||||
|
||||
updateLockIcon(id) {
|
||||
@ -803,6 +962,7 @@
|
||||
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');
|
||||
@ -814,16 +974,23 @@
|
||||
}
|
||||
|
||||
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 +1002,7 @@
|
||||
if (unitSelect) {
|
||||
unitSelect.value = selectedUnit;
|
||||
this.updateFoodCalculations();
|
||||
this.saveStateToStorage();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -950,58 +1118,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 +1282,91 @@
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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'); }
|
||||
|
||||
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';
|
||||
|
||||
this.currentAge = age;
|
||||
this.updateFoodCalculations();
|
||||
}
|
||||
|
||||
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';
|
||||
// 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: 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
|
||||
// 5–6 mo (5.0–6.0): 1250→1550; (6.0–<7.0): hold 1550
|
||||
// 7–12 mo (7.0–12.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 {
|
||||
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');
|
||||
}
|
||||
const t = (age - 7) / (12 - 7);
|
||||
return 1300 + t * (1500 - 1300);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
updateCupsButtonState() {
|
||||
// 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 +1405,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
|
||||
@ -1301,102 +1429,86 @@
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 === 'gc') {
|
||||
chartGrams = age !== null ? this.getGCChartGramsForAge(age) : 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 === 'gc' || 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 +1528,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 +1585,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 +1625,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 +1693,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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user