Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions SparkyFitnessFrontend/src/components/EnhancedFoodSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { UserCustomNutrient } from "@/types/customNutrient"; // Add import
interface OpenFoodFactsProduct {
product_name: string;
brands?: string;
serving_quantity?: number;
nutriments: {
"energy-kcal_100g"?: number;
proteins_100g?: number;
Expand Down Expand Up @@ -198,6 +199,7 @@ const EnhancedFoodSearch = ({
energyUnit,
convertEnergy,
getEnergyUnitString,
autoScaleOpenFoodFactsImports, // Add auto-scale preference
} = usePreferences(); // Get loggingLevel, itemDisplayLimit, and foodDisplayLimit
const isMobile = useIsMobile();
const platform = isMobile ? "mobile" : "desktop";
Expand Down Expand Up @@ -395,23 +397,29 @@ const EnhancedFoodSearch = ({
};

const convertOpenFoodFactsToFood = (product: OpenFoodFactsProduct): Food => {
// Calculate scaling factor based on serving_quantity (defaults to 100g if not provided)
// Only apply scaling if autoScaleOpenFoodFactsImports preference is enabled
const shouldScale = autoScaleOpenFoodFactsImports && product.serving_quantity && product.serving_quantity > 0;
const servingSize = shouldScale ? product.serving_quantity! : 100;
const scaleFactor = shouldScale ? servingSize / 100 : 1; // Scale from 100g to actual serving size, or 1 if disabled

const defaultVariant: FoodVariant = {
id: "default", // Assign a default ID for now
serving_size: 100,
serving_size: servingSize,
serving_unit: "g",
calories: Math.round(product.nutriments["energy-kcal_100g"] || 0), // Assumed to be in kcal
protein: Math.round((product.nutriments["proteins_100g"] || 0) * 10) / 10,
calories: Math.round((product.nutriments["energy-kcal_100g"] || 0) * scaleFactor), // Assumed to be in kcal, and scaled from 100g to serving
protein: Math.round((product.nutriments["proteins_100g"] || 0) * scaleFactor * 10) / 10,
carbs:
Math.round((product.nutriments["carbohydrates_100g"] || 0) * 10) / 10,
fat: Math.round((product.nutriments["fat_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["carbohydrates_100g"] || 0) * scaleFactor * 10) / 10,
fat: Math.round((product.nutriments["fat_100g"] || 0) * scaleFactor * 10) / 10,
saturated_fat:
Math.round((product.nutriments["saturated-fat_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["saturated-fat_100g"] || 0) * scaleFactor * 10) / 10,
sodium: product.nutriments["sodium_100g"]
? Math.round(product.nutriments["sodium_100g"] * 1000)
? Math.round(product.nutriments["sodium_100g"] * 1000 * scaleFactor)
: 0,
dietary_fiber:
Math.round((product.nutriments["fiber_100g"] || 0) * 10) / 10,
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * 10) / 10,
Math.round((product.nutriments["fiber_100g"] || 0) * scaleFactor * 10) / 10,
sugars: Math.round((product.nutriments["sugars_100g"] || 0) * scaleFactor * 10) / 10,
// Initialize other nutrients to 0 or appropriate defaults
polyunsaturated_fat: 0,
monounsaturated_fat: 0,
Expand All @@ -423,6 +431,7 @@ const EnhancedFoodSearch = ({
calcium: 0,
iron: 0,
is_default: true,
is_locked: shouldScale, // Enable auto-scale only if preference is enabled and scaling was applied
glycemic_index: "None", // Default GI for OpenFoodFacts
};

Expand Down
15 changes: 15 additions & 0 deletions SparkyFitnessFrontend/src/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; // Added import
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
Expand Down Expand Up @@ -67,6 +68,7 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
dateFormat, setDateFormat,
loggingLevel, setLoggingLevel,
itemDisplayLimit, setItemDisplayLimit, // Add itemDisplayLimit and setItemDisplayLimit
autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImports, // Add auto-scale preference
loadPreferences: loadUserPreferencesFromContext, // Rename to avoid conflict
saveAllPreferences, // Add saveAllPreferences from context
formatDate, // Destructure formatDate
Expand Down Expand Up @@ -741,6 +743,19 @@ const Settings: React.FC<SettingsProps> = ({ onShowAboutDialog }) => {
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between col-span-2 py-2">
<div className="space-y-0.5">
<Label htmlFor="auto-scale-openfoodfacts">{t('settings.preferences.autoScaleOpenFoodFacts', 'Auto-scale OpenFoodFacts Imports')}</Label>
<p className="text-sm text-muted-foreground">
{t('settings.preferences.autoScaleOpenFoodFactsHint', 'When enabled, nutrition values from OpenFoodFacts will be automatically scaled from per-100g to the product\'s serving size.')}
</p>
</div>
<Switch
id="auto-scale-openfoodfacts"
checked={autoScaleOpenFoodFactsImports}
onCheckedChange={setAutoScaleOpenFoodFactsImports}
/>
</div>
</div>
<Button onClick={handlePreferencesUpdate} disabled={loading}>
<Save className="h-4 w-4 mr-2" />
Expand Down
24 changes: 24 additions & 0 deletions SparkyFitnessFrontend/src/contexts/PreferencesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ interface PreferencesContextType {
itemDisplayLimit: number;
calorieGoalAdjustmentMode: 'dynamic' | 'fixed'; // Add new preference
energyUnit: EnergyUnit; // Add energy unit
autoScaleOpenFoodFactsImports: boolean; // Auto-scale nutrition from 100g to serving size
nutrientDisplayPreferences: NutrientPreference[];
water_display_unit: 'ml' | 'oz' | 'liter';
language: string;
Expand All @@ -97,6 +98,7 @@ interface PreferencesContextType {
setItemDisplayLimit: (limit: number) => void;
setCalorieGoalAdjustmentMode: (mode: 'dynamic' | 'fixed') => void; // Add setter for calorie goal adjustment mode
setEnergyUnit: (unit: EnergyUnit) => void; // Add setter for energy unit
setAutoScaleOpenFoodFactsImports: (enabled: boolean) => void; // Add boolean for auto-scale
loadNutrientDisplayPreferences: () => Promise<void>;
setWaterDisplayUnit: (unit: 'ml' | 'oz' | 'liter') => void;
setLanguage: (language: string) => void;
Expand Down Expand Up @@ -144,6 +146,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
const [foodDisplayLimit, setFoodDisplayLimitState] = useState<number>(10); // Add state for foodDisplayLimit
const [calorieGoalAdjustmentMode, setCalorieGoalAdjustmentModeState] = useState<'dynamic' | 'fixed'>('dynamic'); // New state for calorie goal adjustment
const [energyUnit, setEnergyUnitState] = useState<EnergyUnit>('kcal'); // Add state for energy unit
const [autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImportsState] = useState<boolean>(false); // Auto-scale OpenFoodFacts imports
const [nutrientDisplayPreferences, setNutrientDisplayPreferences] = useState<NutrientPreference[]>([]);
const [waterDisplayUnit, setWaterDisplayUnitState] = useState<'ml' | 'oz' | 'liter'>('ml');
const [language, setLanguageState] = useState<string>('en');
Expand Down Expand Up @@ -178,6 +181,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
const savedLanguage = localStorage.getItem('language');
const savedCalorieGoalAdjustmentMode = localStorage.getItem('calorieGoalAdjustmentMode') as 'dynamic' | 'fixed';
const savedEnergyUnit = localStorage.getItem('energyUnit') as EnergyUnit; // Load energy unit
const savedAutoScaleOpenFoodFactsImports = localStorage.getItem('autoScaleOpenFoodFactsImports'); // Load auto-scale preference

if (savedWeightUnit) {
setWeightUnitState(savedWeightUnit);
Expand Down Expand Up @@ -207,6 +211,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setEnergyUnitState(savedEnergyUnit);
debug(loggingLevel, "PreferencesProvider: Loaded energyUnit from localStorage:", savedEnergyUnit);
}
if (savedAutoScaleOpenFoodFactsImports !== null) { // Set auto-scale state from localStorage
setAutoScaleOpenFoodFactsImportsState(savedAutoScaleOpenFoodFactsImports === 'true');
debug(loggingLevel, "PreferencesProvider: Loaded autoScaleOpenFoodFactsImports from localStorage:", savedAutoScaleOpenFoodFactsImports);
}
}
}
}, [user, loading]); // Add loading to dependency array
Expand Down Expand Up @@ -236,6 +244,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setLanguageState(data.language || 'en');
setCalorieGoalAdjustmentModeState(data.calorie_goal_adjustment_mode || 'dynamic'); // Set calorie goal adjustment mode state
setEnergyUnitState(data.energy_unit as EnergyUnit || 'kcal'); // Set energy unit state, default to kcal
setAutoScaleOpenFoodFactsImportsState(data.auto_scale_open_food_facts_imports ?? false); // Set auto-scale state, default to false
setBmrAlgorithmState((data.bmr_algorithm as BmrAlgorithm) || BmrAlgorithm.MIFFLIN_ST_JEOR);
setBodyFatAlgorithmState((data.body_fat_algorithm as BodyFatAlgorithm) || BodyFatAlgorithm.US_NAVY);
setIncludeBmrInNetCaloriesState(data.include_bmr_in_net_calories ?? false);
Expand Down Expand Up @@ -293,6 +302,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: 'en',
calorie_goal_adjustment_mode: 'dynamic', // Add default for new preference
energy_unit: 'kcal', // Add default energy unit
auto_scale_open_food_facts_imports: false, // Add default auto-scale for OpenFoodFacts
selected_diet: 'balanced', // Add default diet
};

Expand Down Expand Up @@ -332,6 +342,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: string;
calorie_goal_adjustment_mode: 'dynamic' | 'fixed';
energy_unit: EnergyUnit;
auto_scale_open_food_facts_imports: boolean;
bmr_algorithm: BmrAlgorithm;
body_fat_algorithm: BodyFatAlgorithm;
include_bmr_in_net_calories: boolean;
Expand Down Expand Up @@ -373,6 +384,10 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
localStorage.setItem('energyUnit', updates.energy_unit);
debug(loggingLevel, "PreferencesProvider: Saved energyUnit to localStorage:", updates.energy_unit);
}
if (updates.auto_scale_open_food_facts_imports !== undefined) { // Save auto-scale to localStorage
localStorage.setItem('autoScaleOpenFoodFactsImports', String(updates.auto_scale_open_food_facts_imports));
debug(loggingLevel, "PreferencesProvider: Saved autoScaleOpenFoodFactsImports to localStorage:", updates.auto_scale_open_food_facts_imports);
}
// default_food_data_provider_id, logging_level and item_display_limit are not stored in localStorage
// food_display_limit is also not stored in localStorage
return;
Expand Down Expand Up @@ -590,6 +605,12 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
saveAllPreferences({ energyUnit: unit }); // Persist the change
};

const setAutoScaleOpenFoodFactsImports = (enabled: boolean) => {
info(loggingLevel, "PreferencesProvider: Setting auto-scale OpenFoodFacts imports to:", enabled);
setAutoScaleOpenFoodFactsImportsState(enabled);
saveAllPreferences({ autoScaleOpenFoodFactsImports: enabled }); // Persist the change
};

const saveAllPreferences = async (newPrefs?: Partial<PreferencesContextType>) => {
info(loggingLevel, "PreferencesProvider: Saving all preferences to backend.");

Expand All @@ -608,6 +629,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
language: newPrefs?.language ?? language,
calorie_goal_adjustment_mode: newPrefs?.calorieGoalAdjustmentMode ?? calorieGoalAdjustmentMode, // Include new preference
energy_unit: newPrefs?.energyUnit ?? energyUnit, // Include energy unit preference
auto_scale_open_food_facts_imports: newPrefs?.autoScaleOpenFoodFactsImports ?? autoScaleOpenFoodFactsImports, // Include auto-scale preference
bmr_algorithm: newPrefs?.bmrAlgorithm ?? bmrAlgorithm,
body_fat_algorithm: newPrefs?.bodyFatAlgorithm ?? bodyFatAlgorithm,
include_bmr_in_net_calories: newPrefs?.includeBmrInNetCalories ?? includeBmrInNetCalories,
Expand Down Expand Up @@ -665,6 +687,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
foodDisplayLimit, // Expose foodDisplayLimit
calorieGoalAdjustmentMode, // Expose new preference
energyUnit, // Expose energyUnit
autoScaleOpenFoodFactsImports, // Expose auto-scale OpenFoodFacts imports
nutrientDisplayPreferences,
water_display_unit: waterDisplayUnit,
language,
Expand All @@ -679,6 +702,7 @@ export const PreferencesProvider: React.FC<{ children: React.ReactNode }> = ({ c
setItemDisplayLimit,
setCalorieGoalAdjustmentMode, // Expose new setter
setEnergyUnit, // Expose setEnergyUnit
setAutoScaleOpenFoodFactsImports, // Expose setAutoScaleOpenFoodFactsImports
loadNutrientDisplayPreferences,
setWaterDisplayUnit: setWaterDisplayUnitState,
setLanguage: setLanguageState,
Expand Down
42 changes: 41 additions & 1 deletion SparkyFitnessFrontend/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { usePreferences } from "@/contexts/PreferencesContext";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { useTranslation } from "react-i18next";
import { toast } from "@/hooks/use-toast";
import CustomNutrientsSettings from './CustomNutrientsSettings';

const Settings = () => {
const { t } = useTranslation();
const { energyUnit, setEnergyUnit, saveAllPreferences } = usePreferences();
const { energyUnit, setEnergyUnit, autoScaleOpenFoodFactsImports, setAutoScaleOpenFoodFactsImports, saveAllPreferences } = usePreferences();

const handleEnergyUnitChange = async (unit: 'kcal' | 'kJ') => {
try {
Expand All @@ -29,6 +30,23 @@ const Settings = () => {
}
};

const handleAutoScaleChange = async (enabled: boolean) => {
try {
await setAutoScaleOpenFoodFactsImports(enabled);
toast({
title: t("settings.autoScale.successTitle", "Success"),
description: t("settings.autoScale.successDescription", "Auto-scale preference updated successfully."),
});
} catch (error) {
console.error("Failed to update auto-scale preference:", error);
toast({
title: t("settings.autoScale.errorTitle", "Error"),
description: t("settings.autoScale.errorDescription", "Failed to update auto-scale preference."),
variant: "destructive",
});
}
};

return (
<div className="space-y-6 p-4 md:p-8">
<h1 className="text-3xl font-bold">{t("settings.title", "Settings")}</h1>
Expand Down Expand Up @@ -66,6 +84,28 @@ const Settings = () => {
</div>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>{t("settings.foodImport.title", "Food Import")}</CardTitle>
<CardDescription>{t("settings.foodImport.description", "Configure how food data is imported from external sources.")}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="auto-scale-openfoodfacts">{t("settings.foodImport.autoScaleLabel", "Auto-scale OpenFoodFacts Imports")}</Label>
<p className="text-sm text-muted-foreground">
{t("settings.foodImport.autoScaleHint", "When enabled, nutrition values from OpenFoodFacts will be automatically scaled from per-100g to the product's serving size.")}
</p>
</div>
<Switch
id="auto-scale-openfoodfacts"
checked={autoScaleOpenFoodFactsImports}
onCheckedChange={handleAutoScaleChange}
/>
</div>
</CardContent>
</Card>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Migration: Add auto_scale_open_food_facts_imports column to user_preferences table
-- Preference controls whether OpenFoodFacts imports should automatically scale
-- Nutrition values from per-100g to the actual serving size via OpenFoodFacts

ALTER TABLE user_preferences
ADD COLUMN IF NOT EXISTS auto_scale_open_food_facts_imports BOOLEAN DEFAULT FALSE;

COMMENT ON COLUMN user_preferences.auto_scale_open_food_facts_imports IS 'When enabled, OpenFoodFacts imports will automatically scale nutrition values from per-100g to the serving size provided by the product';
Loading