Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 68d5527a89 | |||
| 19592f2230 | |||
| d0a30a8f8d | |||
| 374d067cf4 | |||
| 73c4648978 | |||
| da9fd20ffb | |||
| d489a87722 | |||
| 7fd139b321 |
@@ -0,0 +1,43 @@
|
|||||||
|
Kaya’s 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 Kaya’s 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 (as‑fed) for accuracy.
|
||||||
|
- Monitor body condition and stool; adjust if needed.
|
||||||
|
|
||||||
|
|
||||||
|
What you’ll see
|
||||||
|
---------------
|
||||||
|
- Exact amounts for each food based on your percentages and energy labels.
|
||||||
|
- Totals per day (or per meal), and optional multi‑day batches.
|
||||||
|
- Everything updates instantly as you change inputs.
|
||||||
|
|
||||||
|
Good to know
|
||||||
|
------------
|
||||||
|
- Gently cooked is less calorie‑dense 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, double‑check 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.
|
||||||
@@ -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 Kaya’s 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 Kaya’s age in months (2.0–12.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 per‑day or per‑meal, 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 Kaya’s exact age (month‑level interpolation), not from generic MER.
|
||||||
|
- That daily kcal target is split across your foods by percentage and converted into amounts using each food’s energy density.
|
||||||
|
- Results include per‑food amounts and totals, per‑day or per‑meal, 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)
|
||||||
|
- Per‑food kcal = daily_kcal × (food_percentage ÷ 100)
|
||||||
|
- Per‑food grams (kcal/100 g) = per‑food_kcal ÷ (kcal_per_100g ÷ 100)
|
||||||
|
- Per‑food grams (kcal/kg) = per‑food_kcal ÷ (kcal_per_kg ÷ 1000)
|
||||||
|
- Cups (when kcal/cup provided) = per‑food_kcal ÷ kcal_per_cup
|
||||||
|
- Internal assumptions for conversions: 1 cup ≈ 120 g (dry), 1 can ≈ 450 g (wet)
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
- Age input is limited to 2–12 months; values are rounded for display (per‑meal 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 fine‑tune based on Kaya’s body condition and response.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+429
-336
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,14 @@
|
|||||||
color: var(--text-label);
|
color: var(--text-label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Kaya end-weight readonly field: compact, non-editable */
|
||||||
|
#kayaEndWeight {
|
||||||
|
width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.dog-calculator-results {
|
.dog-calculator-results {
|
||||||
background: linear-gradient(135deg, rgba(241, 154, 95, 0.08) 0%, rgba(241, 154, 95, 0.04) 100%);
|
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);
|
border: 1px solid rgba(241, 154, 95, 0.2);
|
||||||
|
|||||||
+11
-75
@@ -1,50 +1,26 @@
|
|||||||
<div class="dog-calculator-container" id="dogCalculator">
|
<div class="dog-calculator-container" id="dogCalculator">
|
||||||
<div class="dog-calculator-section">
|
<div class="dog-calculator-section">
|
||||||
<div class="dog-calculator-section-header">
|
<div class="dog-calculator-section-header">
|
||||||
<h2>Dog's Characteristics</h2>
|
<h2>Kaya’s Transition</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dog-calculator-form-group">
|
<div class="dog-calculator-form-group">
|
||||||
<label for="dogType">Dog Type / Activity Level:</label>
|
<label for="ageMonths">Kaya’s age (months):</label>
|
||||||
<select id="dogType" aria-describedby="dogTypeHelp">
|
<input type="number" id="ageMonths" min="2" max="12" step="0.1" placeholder="Enter age in months" aria-describedby="ageHelp">
|
||||||
<option value="">Select dog type...</option>
|
<div id="ageClampNote" class="dog-calculator-error dog-calculator-hidden">Age adjusted to the supported 2–12 month range.</div>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="dog-calculator-form-group">
|
<div class="dog-calculator-form-group">
|
||||||
<label for="weight" id="weightLabel">Dog's Weight (kg):</label>
|
<label for="weight" id="weightLabel">Kaya’s current weight (kg):</label>
|
||||||
<input type="number" id="weight" min="0.1" step="0.1" placeholder="Enter weight in kg" aria-describedby="weightHelp">
|
<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 id="weightError" class="dog-calculator-error dog-calculator-hidden">Please enter a valid weight (minimum 0.1 kg)</div>
|
||||||
</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>
|
<div class="dog-calculator-form-group">
|
||||||
<span class="dog-calculator-result-value" id="rerValue">- cal/day</span>
|
<label for="kayaEndWeight">Kaya’s end‑weight:</label>
|
||||||
</div>
|
<input type="text" id="kayaEndWeight" value="30 kg" readonly>
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,9 +74,6 @@
|
|||||||
<div class="dog-calculator-unit-buttons" id="unitButtons" style="display: none;">
|
<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 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="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>
|
</div>
|
||||||
|
|
||||||
<!-- Daily Total Results -->
|
<!-- Daily Total Results -->
|
||||||
@@ -115,9 +88,6 @@
|
|||||||
<select id="unit" class="dog-calculator-unit-select-hidden" aria-describedby="unitHelp">
|
<select id="unit" class="dog-calculator-unit-select-hidden" aria-describedby="unitHelp">
|
||||||
<option value="g">grams (g)</option>
|
<option value="g">grams (g)</option>
|
||||||
<option value="kg">kilograms (kg)</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>
|
</select>
|
||||||
|
|
||||||
<div class="dog-calculator-food-amounts-section" id="foodAmountsSection" style="display: none;">
|
<div class="dog-calculator-food-amounts-section" id="foodAmountsSection" style="display: none;">
|
||||||
@@ -139,44 +109,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="dog-calculator-footer">
|
||||||
<a href="https://caninenutritionandwellness.com" target="_blank" rel="noopener noreferrer">
|
<a href="https://caninenutritionandwellness.com" target="_blank" rel="noopener noreferrer">
|
||||||
by caninenutritionandwellness.com
|
by caninenutritionandwellness.com
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|||||||
+403
-260
@@ -15,15 +15,22 @@
|
|||||||
this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources;
|
this.maxFoodSources = CALCULATOR_CONFIG.maxFoodSources;
|
||||||
this.mealsPerDay = 2;
|
this.mealsPerDay = 2;
|
||||||
this.showPerMeal = false;
|
this.showPerMeal = false;
|
||||||
|
// Kayafied reference source tracking
|
||||||
|
this.kibbleRefId = null;
|
||||||
|
this.fredRefId = null;
|
||||||
|
this.storageKey = 'kaya_calculator_state_v1';
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.applyTheme();
|
this.applyTheme();
|
||||||
this.applyScale();
|
this.applyScale();
|
||||||
this.initializeFoodSources();
|
|
||||||
this.bindEvents();
|
|
||||||
this.updateUnitLabels();
|
this.updateUnitLabels();
|
||||||
|
const restored = this.loadStateFromStorage();
|
||||||
|
if (!restored) {
|
||||||
|
this.initializeFoodSources();
|
||||||
|
}
|
||||||
|
this.bindEvents();
|
||||||
this.setupIframeResize();
|
this.setupIframeResize();
|
||||||
|
|
||||||
// Show the calculator with fade-in
|
// Show the calculator with fade-in
|
||||||
@@ -31,6 +38,129 @@
|
|||||||
container.classList.add('loaded');
|
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() {
|
getThemeFromURL() {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const theme = urlParams.get('theme');
|
const theme = urlParams.get('theme');
|
||||||
@@ -69,8 +199,49 @@
|
|||||||
|
|
||||||
// Food Source Management Methods
|
// Food Source Management Methods
|
||||||
initializeFoodSources() {
|
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.updateAddButton();
|
||||||
|
this.updateRemoveButtons();
|
||||||
|
this.refreshAllPercentageUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
addFoodSource() {
|
addFoodSource() {
|
||||||
@@ -94,6 +265,7 @@
|
|||||||
this.updateAddButton();
|
this.updateAddButton();
|
||||||
this.updateRemoveButtons();
|
this.updateRemoveButtons();
|
||||||
this.refreshAllPercentageUI();
|
this.refreshAllPercentageUI();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFoodSource(id) {
|
removeFoodSource(id) {
|
||||||
@@ -105,6 +277,9 @@
|
|||||||
if (index === -1) return;
|
if (index === -1) return;
|
||||||
|
|
||||||
this.foodSources.splice(index, 1);
|
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
|
// Remove the DOM element
|
||||||
const element = document.getElementById(`foodSource-${id}`);
|
const element = document.getElementById(`foodSource-${id}`);
|
||||||
@@ -118,6 +293,8 @@
|
|||||||
this.updateAddButton();
|
this.updateAddButton();
|
||||||
this.updateRemoveButtons();
|
this.updateRemoveButtons();
|
||||||
this.refreshAllPercentageUI();
|
this.refreshAllPercentageUI();
|
||||||
|
this.updateCalorieCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
generateFoodSourceId() {
|
generateFoodSourceId() {
|
||||||
@@ -399,6 +576,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.saveStateToStorage();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +597,7 @@
|
|||||||
|
|
||||||
// Update food calculations
|
// Update food calculations
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSliderConstraints(foodSource) {
|
updateSliderConstraints(foodSource) {
|
||||||
@@ -511,6 +690,11 @@
|
|||||||
const container = document.getElementById('foodSources');
|
const container = document.getElementById('foodSources');
|
||||||
if (!container) return;
|
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 = `
|
const cardHTML = `
|
||||||
<div class="dog-calculator-food-source-card" id="foodSource-${foodSource.id}">
|
<div class="dog-calculator-food-source-card" id="foodSource-${foodSource.id}">
|
||||||
<div class="dog-calculator-food-source-header">
|
<div class="dog-calculator-food-source-header">
|
||||||
@@ -521,11 +705,11 @@
|
|||||||
<div class="dog-calculator-input-group">
|
<div class="dog-calculator-input-group">
|
||||||
<div class="dog-calculator-form-group">
|
<div class="dog-calculator-form-group">
|
||||||
<label for="energy-${foodSource.id}">Energy Content:</label>
|
<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>
|
||||||
<div class="dog-calculator-form-group">
|
<div class="dog-calculator-form-group">
|
||||||
<label for="energy-unit-${foodSource.id}">Unit:</label>
|
<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="kcal100g" ${foodSource.energyUnit === 'kcal100g' ? 'selected' : ''}>kcal/100g</option>
|
||||||
<option value="kcalkg" ${foodSource.energyUnit === 'kcalkg' ? 'selected' : ''}>kcal/kg</option>
|
<option value="kcalkg" ${foodSource.energyUnit === 'kcalkg' ? 'selected' : ''}>kcal/kg</option>
|
||||||
<option value="kcalcup" ${foodSource.energyUnit === 'kcalcup' ? 'selected' : ''}>kcal/cup</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}`;
|
const newName = nameInput.value.trim() || `Food Source ${this.foodSources.findIndex(fs => fs.id === id) + 1}`;
|
||||||
this.updateFoodSourceData(id, 'name', newName);
|
this.updateFoodSourceData(id, 'name', newName);
|
||||||
this.updateFoodCalculations(); // This will refresh the food amount breakdown with new names
|
this.updateFoodCalculations(); // This will refresh the food amount breakdown with new names
|
||||||
|
this.saveStateToStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
nameInput.addEventListener('blur', () => {
|
nameInput.addEventListener('blur', () => {
|
||||||
@@ -582,50 +767,42 @@
|
|||||||
nameInput.value = defaultName;
|
nameInput.value = defaultName;
|
||||||
this.updateFoodSourceData(id, 'name', defaultName);
|
this.updateFoodSourceData(id, 'name', defaultName);
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (energyInput) {
|
if (energyInput && !energyInput.hasAttribute('readonly')) {
|
||||||
energyInput.addEventListener('input', () => {
|
energyInput.addEventListener('input', () => {
|
||||||
this.updateFoodSourceData(id, 'energy', energyInput.value);
|
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
|
// Auto-select cups when entering energy for kcal/cup
|
||||||
const foodSource = this.foodSources.find(fs => fs.id === id);
|
const foodSource = this.foodSources.find(fs => fs.id === id);
|
||||||
if (foodSource && foodSource.energyUnit === 'kcalcup' && parseFloat(energyInput.value) > 0) {
|
if (foodSource && foodSource.energyUnit === 'kcalcup' && parseFloat(energyInput.value) > 0) {
|
||||||
|
// Cups display removed; default to grams
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
const cupsButton = document.getElementById('cupsButton');
|
if (unitSelect) {
|
||||||
|
unitSelect.value = 'g';
|
||||||
// First check if cups button will be enabled after update
|
unitSelect.setAttribute('value', 'g');
|
||||||
const willEnableCups = this.foodSources.some(fs =>
|
this.setActiveUnitButton('g');
|
||||||
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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now update calculations with cups already selected
|
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
} else {
|
this.saveStateToStorage();
|
||||||
this.updateFoodCalculations();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id));
|
energyInput.addEventListener('blur', () => this.validateFoodSourceEnergy(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (energyUnitSelect) {
|
if (energyUnitSelect && !energyUnitSelect.hasAttribute('disabled')) {
|
||||||
energyUnitSelect.addEventListener('change', () => {
|
energyUnitSelect.addEventListener('change', () => {
|
||||||
this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value);
|
this.updateFoodSourceData(id, 'energyUnit', energyUnitSelect.value);
|
||||||
|
if (id === this.kibbleRefId) {
|
||||||
|
this.updateCalorieCalculations();
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-select the most appropriate unit based on energy unit
|
// Auto-select the most appropriate unit based on energy unit
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
@@ -634,45 +811,38 @@
|
|||||||
if (unitSelect) {
|
if (unitSelect) {
|
||||||
switch(energyUnitSelect.value) {
|
switch(energyUnitSelect.value) {
|
||||||
case 'kcalcup':
|
case 'kcalcup':
|
||||||
// Check if we have energy value to enable cups
|
// Cups display not available; default to grams
|
||||||
const foodSource = this.foodSources.find(fs => fs.id === id);
|
unitSelect.value = 'g';
|
||||||
if (foodSource && foodSource.energy && parseFloat(foodSource.energy) > 0) {
|
this.setActiveUnitButton('g');
|
||||||
// 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();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
break;
|
break;
|
||||||
case 'kcal100g':
|
case 'kcal100g':
|
||||||
// For kcal/100g, select grams
|
// For kcal/100g, select grams
|
||||||
unitSelect.value = 'g';
|
unitSelect.value = 'g';
|
||||||
this.setActiveUnitButton('g');
|
this.setActiveUnitButton('g');
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
break;
|
break;
|
||||||
case 'kcalkg':
|
case 'kcalkg':
|
||||||
// For kcal/kg, also select grams (or could be kg)
|
// For kcal/kg, also select grams (or could be kg)
|
||||||
unitSelect.value = 'g';
|
unitSelect.value = 'g';
|
||||||
this.setActiveUnitButton('g');
|
this.setActiveUnitButton('g');
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
break;
|
break;
|
||||||
case 'kcalcan':
|
case 'kcalcan':
|
||||||
// For kcal/can, use grams as default (or ounces in imperial)
|
// For kcal/can, use grams as default
|
||||||
unitSelect.value = this.isImperial ? 'oz' : 'g';
|
unitSelect.value = 'g';
|
||||||
this.setActiveUnitButton(unitSelect.value);
|
this.setActiveUnitButton('g');
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No unit select, just update calculations
|
// No unit select, just update calculations
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -728,6 +898,7 @@
|
|||||||
this.updateLockIcon(id);
|
this.updateLockIcon(id);
|
||||||
this.updateLockStates();
|
this.updateLockStates();
|
||||||
this.refreshAllPercentageUI();
|
this.refreshAllPercentageUI();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLockIcon(id) {
|
updateLockIcon(id) {
|
||||||
@@ -803,27 +974,35 @@
|
|||||||
bindEvents() {
|
bindEvents() {
|
||||||
const weightInput = document.getElementById('weight');
|
const weightInput = document.getElementById('weight');
|
||||||
const dogTypeSelect = document.getElementById('dogType');
|
const dogTypeSelect = document.getElementById('dogType');
|
||||||
|
const ageInput = document.getElementById('ageMonths');
|
||||||
const daysInput = document.getElementById('days');
|
const daysInput = document.getElementById('days');
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
const unitToggle = document.getElementById('unitToggle');
|
const unitToggle = document.getElementById('unitToggle');
|
||||||
const addFoodBtn = document.getElementById('addFoodBtn');
|
const addFoodBtn = document.getElementById('addFoodBtn');
|
||||||
|
|
||||||
if (weightInput) {
|
if (weightInput) {
|
||||||
weightInput.addEventListener('input', () => this.updateCalorieCalculations());
|
weightInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
|
||||||
weightInput.addEventListener('blur', () => this.validateWeight());
|
weightInput.addEventListener('blur', () => { this.validateWeight(); this.saveStateToStorage(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
|
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
|
||||||
|
|
||||||
|
// Kayafied: age input drives energy target
|
||||||
|
if (ageInput) {
|
||||||
|
ageInput.addEventListener('input', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
|
||||||
|
ageInput.addEventListener('blur', () => { this.updateCalorieCalculations(); this.saveStateToStorage(); });
|
||||||
|
}
|
||||||
|
|
||||||
if (daysInput) {
|
if (daysInput) {
|
||||||
daysInput.addEventListener('input', () => {
|
daysInput.addEventListener('input', () => {
|
||||||
this.updateDayLabel();
|
this.updateDayLabel();
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
});
|
});
|
||||||
daysInput.addEventListener('blur', () => this.validateDays());
|
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
|
// Unit button event listeners
|
||||||
const unitButtons = document.querySelectorAll('.dog-calculator-unit-btn');
|
const unitButtons = document.querySelectorAll('.dog-calculator-unit-btn');
|
||||||
@@ -835,6 +1014,7 @@
|
|||||||
if (unitSelect) {
|
if (unitSelect) {
|
||||||
unitSelect.value = selectedUnit;
|
unitSelect.value = selectedUnit;
|
||||||
this.updateFoodCalculations();
|
this.updateFoodCalculations();
|
||||||
|
this.saveStateToStorage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -950,58 +1130,14 @@
|
|||||||
imperialLabel.classList.toggle('active', this.isImperial);
|
imperialLabel.classList.toggle('active', this.isImperial);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isImperial) {
|
// Kaya: restrict to metric g/kg only
|
||||||
if (weightLabel) weightLabel.textContent = "Dog's Weight (lbs):";
|
|
||||||
if (weightInput) {
|
|
||||||
weightInput.placeholder = "Enter weight in lbs";
|
|
||||||
weightInput.min = "0.2";
|
|
||||||
weightInput.step = "0.1";
|
|
||||||
}
|
|
||||||
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) {
|
if (unitSelect) {
|
||||||
unitSelect.innerHTML = '<option value="g">grams (g)</option>' +
|
unitSelect.innerHTML = '<option value="g">grams (g)</option>' +
|
||||||
'<option value="kg">kilograms (kg)</option>' +
|
'<option value="kg">kilograms (kg)</option>';
|
||||||
'<option value="oz">ounces (oz)</option>' +
|
if (!unitSelect.value || (unitSelect.value !== 'g' && unitSelect.value !== 'kg')) {
|
||||||
'<option value="lb">pounds (lb)</option>';
|
unitSelect.value = 'g';
|
||||||
unitSelect.value = 'g'; // Auto-select grams for metric
|
this.setActiveUnitButton('g');
|
||||||
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() {
|
updateCalorieCalculations() {
|
||||||
const dogTypeSelect = document.getElementById('dogType');
|
// Kaya-specific: only track age and trigger recompute
|
||||||
const calorieResults = document.getElementById('calorieResults');
|
const ageInput = document.getElementById('ageMonths');
|
||||||
const rerValue = document.getElementById('rerValue');
|
const ageClampNote = document.getElementById('ageClampNote');
|
||||||
const merValue = document.getElementById('merValue');
|
|
||||||
|
|
||||||
if (!dogTypeSelect || !calorieResults || !rerValue || !merValue) {
|
if (ageClampNote) ageClampNote.classList.add('dog-calculator-hidden');
|
||||||
|
|
||||||
|
if (!ageInput || ageInput.value === '') {
|
||||||
|
this.currentAge = null;
|
||||||
|
this.updateFoodCalculations();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const weightKg = this.getWeightInKg();
|
let age = parseFloat(ageInput.value);
|
||||||
const dogTypeFactor = dogTypeSelect.value;
|
if (isNaN(age)) {
|
||||||
|
this.currentAge = null;
|
||||||
this.showError('weightError', false);
|
this.updateFoodCalculations();
|
||||||
|
|
||||||
if (!weightKg || weightKg < 0.1) {
|
|
||||||
const weightInput = document.getElementById('weight');
|
|
||||||
if (weightInput && weightInput.value) this.showError('weightError', true);
|
|
||||||
calorieResults.style.display = 'none';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dogTypeFactor) {
|
if (age < 2) { age = 2; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
calorieResults.style.display = 'none';
|
if (age > 12) { age = 12; if (ageClampNote) ageClampNote.classList.remove('dog-calculator-hidden'); }
|
||||||
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';
|
|
||||||
|
|
||||||
|
this.currentAge = age;
|
||||||
this.updateFoodCalculations();
|
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() {
|
updateCupsButtonState() {
|
||||||
const cupsButton = document.getElementById('cupsButton');
|
// Cups UI is not used in this configuration
|
||||||
if (!cupsButton) return;
|
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFoodCalculations() {
|
updateFoodCalculations() {
|
||||||
if (this.currentMER === 0) return;
|
// Chart-first: no MER check
|
||||||
|
const hasRange = false;
|
||||||
// Check if we have a range
|
|
||||||
const hasRange = this.currentMERMin !== this.currentMERMax;
|
|
||||||
|
|
||||||
const daysInput = document.getElementById('days');
|
const daysInput = document.getElementById('days');
|
||||||
const unitSelect = document.getElementById('unit');
|
const unitSelect = document.getElementById('unit');
|
||||||
@@ -1277,7 +1429,7 @@
|
|||||||
// Debug: log what unit is being used
|
// Debug: log what unit is being used
|
||||||
console.log('UpdateFoodCalculations - unit:', unit, 'unitLabel:', unitLabel);
|
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';
|
const frequencySuffix = this.showPerMeal ? '/meal' : '/day';
|
||||||
|
|
||||||
// Clear all food source errors first
|
// Clear all food source errors first
|
||||||
@@ -1285,6 +1437,7 @@
|
|||||||
this.showError(`energy-error-${fs.id}`, false);
|
this.showError(`energy-error-${fs.id}`, false);
|
||||||
});
|
});
|
||||||
this.showError('daysError', false);
|
this.showError('daysError', false);
|
||||||
|
this.showError('weightError', false);
|
||||||
|
|
||||||
// Validate days input
|
// Validate days input
|
||||||
if (!days || !this.validateInput(days, 1, true)) {
|
if (!days || !this.validateInput(days, 1, true)) {
|
||||||
@@ -1302,101 +1455,95 @@
|
|||||||
|
|
||||||
const numDays = parseInt(days);
|
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 = [];
|
const foodBreakdowns = [];
|
||||||
let totalDailyGrams = 0;
|
let totalDailyGrams = 0;
|
||||||
let hasValidFoods = false;
|
let hasValidFoods = false;
|
||||||
|
|
||||||
|
// First pass: charted baseline
|
||||||
|
let chartedKcal = 0;
|
||||||
|
let chartedPercent = 0;
|
||||||
|
const firstPass = [];
|
||||||
this.foodSources.forEach(fs => {
|
this.foodSources.forEach(fs => {
|
||||||
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
const energyPer100g = this.getFoodSourceEnergyPer100g(fs);
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
|
||||||
if (energyPer100g && energyPer100g > 0.1 && fs.percentage > 0) {
|
const kcalPerPercent = chartedPercent > 0 ? (chartedKcal / chartedPercent) : null;
|
||||||
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;
|
// Second pass: finalize amounts
|
||||||
let dailyGramsMin, dailyGramsMax;
|
let splitDailyTotal = 0;
|
||||||
let dailyCupsForThisFood = null;
|
let dailyOnlyTotal = 0;
|
||||||
let dailyCupsMin, dailyCupsMax;
|
firstPass.forEach(({ fs, energyPer100g, gramsPortion }) => {
|
||||||
|
let dailyGramsForThisFood = 0;
|
||||||
// For kcal/cup, calculate cups directly from calories
|
let hasEnergyContent = !!(energyPer100g && energyPer100g > 0);
|
||||||
if (fs.energyUnit === 'kcalcup' && fs.energy) {
|
if ((fs.chartType === 'mer' || fs.chartType === 'kibble')) {
|
||||||
const caloriesPerCup = parseFloat(fs.energy);
|
if (gramsPortion !== null) {
|
||||||
dailyCupsForThisFood = dailyCaloriesForThisFood / caloriesPerCup;
|
dailyGramsForThisFood = gramsPortion;
|
||||||
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 {
|
} else {
|
||||||
// For other units, calculate grams normally
|
if (hasEnergyContent && kcalPerPercent && (fs.percentage || 0) > 0) {
|
||||||
dailyGramsForThisFood = (dailyCaloriesForThisFood / energyPer100g) * 100;
|
const perGramKcal = energyPer100g / 100;
|
||||||
dailyGramsMin = (dailyCaloriesMin / energyPer100g) * 100;
|
const kcalForFood = (fs.percentage || 0) * kcalPerPercent;
|
||||||
dailyGramsMax = (dailyCaloriesMax / energyPer100g) * 100;
|
dailyGramsForThisFood = kcalForFood / perGramKcal;
|
||||||
|
} else {
|
||||||
|
hasEnergyContent = false;
|
||||||
|
dailyGramsForThisFood = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate per-meal amounts if needed
|
const isDailyOnly = fs.splitByMeals === false;
|
||||||
const displayGrams = this.showPerMeal ? dailyGramsForThisFood / this.mealsPerDay : dailyGramsForThisFood;
|
const displayGrams = (this.showPerMeal && !isDailyOnly) ? (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({
|
foodBreakdowns.push({
|
||||||
name: fs.name,
|
name: fs.name,
|
||||||
percentage: fs.percentage,
|
percentage: fs.percentage,
|
||||||
dailyGrams: dailyGramsForThisFood,
|
dailyGrams: dailyGramsForThisFood,
|
||||||
dailyGramsMin: dailyGramsMin,
|
isDailyOnly: isDailyOnly,
|
||||||
dailyGramsMax: dailyGramsMax,
|
|
||||||
displayGrams: displayGrams,
|
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,
|
dailyCups: null,
|
||||||
displayCups: null,
|
displayCups: null,
|
||||||
calories: 0,
|
calories: hasEnergyContent ? (dailyGramsForThisFood * (energyPer100g / 100)) : 0,
|
||||||
displayCalories: 0,
|
displayCalories: hasEnergyContent ? (this.showPerMeal ? (dailyGramsForThisFood * (energyPer100g / 100)) / this.mealsPerDay : (dailyGramsForThisFood * (energyPer100g / 100))) : 0,
|
||||||
isLocked: fs.isLocked,
|
isLocked: fs.isLocked,
|
||||||
hasEnergyContent: false,
|
hasEnergyContent: hasEnergyContent,
|
||||||
foodSource: fs // Store reference for cups conversion
|
foodSource: fs
|
||||||
});
|
});
|
||||||
}
|
totalDailyGrams += dailyGramsForThisFood;
|
||||||
|
if (isDailyOnly) dailyOnlyTotal += dailyGramsForThisFood; else splitDailyTotal += dailyGramsForThisFood;
|
||||||
|
if (dailyGramsForThisFood > 0) hasValidFoods = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!hasValidFoods) {
|
if (!hasValidFoods) {
|
||||||
@@ -1416,12 +1563,13 @@
|
|||||||
const unitButtons = document.getElementById('unitButtons');
|
const unitButtons = document.getElementById('unitButtons');
|
||||||
if (unitButtons) unitButtons.style.display = 'none';
|
if (unitButtons) unitButtons.style.display = 'none';
|
||||||
|
|
||||||
// If we have any food sources without energy content, still show the breakdown section
|
// If we have any foods with >0% but missing energy, show warnings only for those
|
||||||
if (foodBreakdowns.length > 0) {
|
const visibleBreakdownsMissing = foodBreakdowns.filter(b => b.percentage > 0);
|
||||||
|
if (visibleBreakdownsMissing.length > 0) {
|
||||||
// Show food amounts section with warnings for missing energy content
|
// Show food amounts section with warnings for missing energy content
|
||||||
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
|
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>' : '';
|
const lockIndicator = breakdown.isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -1472,31 +1620,24 @@
|
|||||||
const unitButtons = document.getElementById('unitButtons');
|
const unitButtons = document.getElementById('unitButtons');
|
||||||
if (unitButtons) unitButtons.style.display = 'flex';
|
if (unitButtons) unitButtons.style.display = 'flex';
|
||||||
|
|
||||||
// Update per-food breakdown
|
// Update per-food breakdown (show only items with >0%)
|
||||||
if (foodBreakdownList && foodBreakdowns.length > 1) {
|
const visibleBreakdowns = foodBreakdowns.filter(b => b.percentage > 0);
|
||||||
const breakdownHTML = foodBreakdowns.map(breakdown => {
|
if (foodBreakdownList && visibleBreakdowns.length > 0) {
|
||||||
|
const breakdownHTML = visibleBreakdowns.map(breakdown => {
|
||||||
let valueContent;
|
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 (breakdown.hasEnergyContent) {
|
||||||
if (unit === 'cups') {
|
if (unit === 'cups') {
|
||||||
// For cups, use the pre-calculated cups value if available
|
// For cups, use the pre-calculated cups value if available
|
||||||
if (breakdown.displayCups !== null) {
|
if (breakdown.displayCups !== null) {
|
||||||
if (breakdown.hasRange && breakdown.displayCupsMin !== breakdown.displayCupsMax) {
|
valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${itemSuffix}`;
|
||||||
valueContent = `${this.formatNumber(breakdown.displayCupsMin, decimals)}-${this.formatNumber(breakdown.displayCupsMax, decimals)} ${unitLabel}${frequencySuffix}`;
|
|
||||||
} else {
|
|
||||||
valueContent = `${this.formatNumber(breakdown.displayCups, decimals)} ${unitLabel}${frequencySuffix}`;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
valueContent = `<span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span>`;
|
valueContent = `<span class="dog-calculator-warning" title="Cups only available for foods with kcal/cup measurement">N/A</span>`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For other units (g, kg, oz, lb)
|
// For other units (g, kg, oz, lb)
|
||||||
if (breakdown.hasRange && breakdown.displayGramsMin !== breakdown.displayGramsMax) {
|
valueContent = `${this.formatNumber(this.convertUnits(breakdown.displayGrams, unit), decimals)} ${unitLabel}${itemSuffix}`;
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
valueContent = `<span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span>`;
|
valueContent = `<span class="dog-calculator-warning" title="Enter energy content to calculate amount">⚠️</span>`;
|
||||||
@@ -1519,7 +1660,8 @@
|
|||||||
// Generate individual food amount breakdown
|
// Generate individual food amount breakdown
|
||||||
|
|
||||||
// Update daily food value with correct units
|
// 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 convertedTotal;
|
||||||
let totalDisplayText;
|
let totalDisplayText;
|
||||||
|
|
||||||
@@ -1586,7 +1728,8 @@
|
|||||||
dailyFoodValue.textContent = totalDisplayText;
|
dailyFoodValue.textContent = totalDisplayText;
|
||||||
|
|
||||||
// Build HTML for individual food amounts
|
// 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>' : '';
|
const lockIndicator = breakdown.isLocked ? '<span class="dog-calculator-lock-indicator">🔒</span>' : '';
|
||||||
|
|
||||||
if (!breakdown.hasEnergyContent) {
|
if (!breakdown.hasEnergyContent) {
|
||||||
|
|||||||
+7
-1
@@ -7,5 +7,11 @@ const CALCULATOR_CONFIG = {
|
|||||||
defaultScale: 1.0,
|
defaultScale: 1.0,
|
||||||
maxFoodSources: 5,
|
maxFoodSources: 5,
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
maxScale: 2.0
|
maxScale: 2.0,
|
||||||
|
|
||||||
|
// Kaya fork: Fred & Felia uses MER; kibble stays chart-based (30 kg column).
|
||||||
|
// Used only when weight input is not present (back-compat).
|
||||||
|
kayaMerDefaultWeightKg: 30,
|
||||||
|
kayaMerFactorUnder4Months: 3.0,
|
||||||
|
kayaMerFactorFrom4Months: 2.0
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user