feat: major update - medical profiles, global diet, gym/home/none workouts, exercise GIFs
Some checks failed
CI/CD - Build, Push & Deploy / Build & Push Docker Image (push) Has been cancelled
CI/CD - Build, Push & Deploy / Update GitOps Manifest (push) Has been cancelled

- Expanded profile with 70+ fields: disability, chronic diseases, medications,
  blood type/pressure/sugar, heart/diabetes/thyroid/asthma/arthritis conditions
- Workout location modes: gym (full equipment), home (bodyweight+equipment),
  none (light activity plan for non-exercisers)
- Exercise database with GIF URLs for visual guidance
- Global diet programs replacing regional foods: Mediterranean, High-Protein,
  Low-Carb, Anti-Inflammatory, Heart-Healthy, Diabetic-Friendly
- Medical condition-aware nutrition (diabetes, heart, blood pressure adjustments)
- 7-step profile wizard with disability and medical history sections
- Fixed DROP TABLE bug in database.js
- Shopping list supports new ingredient format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 13:06:39 +00:00
parent c17f143a40
commit 8b0e2625f5
7 changed files with 6954 additions and 1384 deletions

View File

@@ -29,16 +29,85 @@ function initTables() {
CREATE TABLE IF NOT EXISTS profiles ( CREATE TABLE IF NOT EXISTS profiles (
user_id INTEGER PRIMARY KEY, user_id INTEGER PRIMARY KEY,
-- Basic metrics
age INTEGER, age INTEGER,
gender TEXT, gender TEXT,
height REAL, height REAL,
weight REAL, weight REAL,
target_weight REAL,
-- Body composition
body_fat_percentage REAL,
waist_circumference REAL,
hip_circumference REAL,
neck_circumference REAL,
wrist_circumference REAL,
chest_circumference REAL,
arm_circumference REAL,
thigh_circumference REAL,
calf_circumference REAL,
body_type TEXT,
-- Lifestyle
activity_level TEXT DEFAULT 'moderate', activity_level TEXT DEFAULT 'moderate',
country TEXT DEFAULT 'Turkey',
city TEXT DEFAULT '',
goal TEXT DEFAULT 'maintain', goal TEXT DEFAULT 'maintain',
job_type TEXT,
sleep_hours REAL,
sleep_quality TEXT DEFAULT 'moderate',
water_intake REAL,
stress_level TEXT DEFAULT 'medium',
daily_steps INTEGER,
-- Workout preferences
workout_experience TEXT DEFAULT 'beginner',
workout_days_per_week INTEGER DEFAULT 3,
workout_location TEXT DEFAULT 'gym',
workout_duration_minutes INTEGER DEFAULT 60,
has_equipment INTEGER DEFAULT 0,
available_equipment TEXT DEFAULT '',
fitness_goal TEXT DEFAULT 'general_fitness',
cardio_preference TEXT DEFAULT 'moderate',
-- Injury & disability
has_injury INTEGER DEFAULT 0,
injury_area TEXT DEFAULT 'none',
injury_severity TEXT,
injury_notes TEXT,
has_disability INTEGER DEFAULT 0,
disability_type TEXT DEFAULT 'none',
disability_details TEXT,
mobility_level TEXT DEFAULT 'full',
uses_wheelchair INTEGER DEFAULT 0,
has_prosthetic INTEGER DEFAULT 0,
prosthetic_area TEXT,
-- Medical conditions
health_conditions TEXT DEFAULT '',
chronic_diseases TEXT DEFAULT '',
medications TEXT DEFAULT '',
surgeries TEXT DEFAULT '',
blood_type TEXT,
blood_pressure TEXT DEFAULT 'normal',
blood_sugar TEXT DEFAULT 'normal',
cholesterol_level TEXT DEFAULT 'normal',
heart_condition INTEGER DEFAULT 0,
heart_condition_details TEXT,
has_diabetes INTEGER DEFAULT 0,
diabetes_type TEXT,
has_thyroid INTEGER DEFAULT 0,
thyroid_type TEXT,
has_asthma INTEGER DEFAULT 0,
has_epilepsy INTEGER DEFAULT 0,
has_arthritis INTEGER DEFAULT 0,
arthritis_type TEXT,
has_osteoporosis INTEGER DEFAULT 0,
has_fibromyalgia INTEGER DEFAULT 0,
mental_health TEXT DEFAULT 'none',
-- Nutrition
meals_per_day INTEGER DEFAULT 4,
allergies TEXT DEFAULT '', allergies TEXT DEFAULT '',
dietary_restrictions TEXT DEFAULT '', dietary_restrictions TEXT DEFAULT '',
food_intolerances TEXT DEFAULT '',
supplement_use TEXT DEFAULT '',
caffeine_intake TEXT DEFAULT 'moderate',
smoking TEXT DEFAULT 'no',
alcohol TEXT DEFAULT 'none',
-- Timestamps
updated_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
); );

File diff suppressed because it is too large Load Diff

View File

@@ -6,66 +6,270 @@ const { authenticateToken } = require("../middleware/auth");
const router = express.Router(); const router = express.Router();
// Validation constants
const VALID_GENDERS = ["male", "female"];
const VALID_ACTIVITY_LEVELS = ["sedentary", "light", "moderate", "active", "very_active"];
const VALID_GOALS = ["lose_weight", "gain_weight", "build_muscle", "maintain"];
const VALID_BODY_TYPES = ["ectomorph", "mesomorph", "endomorph"];
const VALID_STRESS_LEVELS = ["low", "medium", "high"];
const VALID_JOB_TYPES = ["desk", "standing", "physical", "mixed"];
const VALID_WORKOUT_EXPERIENCE = ["beginner", "intermediate", "advanced"];
const VALID_WORKOUT_LOCATIONS = ["gym", "home", "none"];
const VALID_FITNESS_GOALS = ["general_fitness", "strength", "endurance", "flexibility", "weight_loss", "muscle_gain", "rehabilitation"];
const VALID_CARDIO_PREFERENCES = ["low", "moderate", "high", "none"];
const VALID_INJURY_AREAS = ["knee", "back", "shoulder", "elbow", "wrist", "ankle", "hip", "neck", "none"];
const VALID_INJURY_SEVERITY = ["mild", "moderate", "severe"];
const VALID_DISABILITY_TYPES = ["none", "visual", "hearing", "motor", "cognitive", "multiple"];
const VALID_MOBILITY_LEVELS = ["full", "limited_upper", "limited_lower", "limited_both", "wheelchair", "bedbound"];
const VALID_BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
const VALID_BP_LEVELS = ["low", "normal", "elevated", "high"];
const VALID_SUGAR_LEVELS = ["low", "normal", "pre_diabetic", "high"];
const VALID_CHOLESTEROL_LEVELS = ["normal", "borderline", "high"];
const VALID_DIABETES_TYPES = ["type1", "type2", "gestational"];
const VALID_THYROID_TYPES = ["hypothyroid", "hyperthyroid"];
const VALID_ARTHRITIS_TYPES = ["rheumatoid", "osteoarthritis", "psoriatic"];
const VALID_MENTAL_HEALTH = ["none", "anxiety", "depression", "both", "other"];
const VALID_SLEEP_QUALITY = ["poor", "moderate", "good"];
const VALID_CAFFEINE = ["none", "low", "moderate", "high"];
const VALID_SMOKING = ["yes", "no", "quit"];
const VALID_ALCOHOL = ["none", "occasional", "moderate", "heavy"];
const PROFILE_FIELDS = [
// Basic
"age", "gender", "height", "weight", "target_weight",
// Body composition
"body_fat_percentage", "waist_circumference", "hip_circumference",
"neck_circumference", "wrist_circumference", "chest_circumference",
"arm_circumference", "thigh_circumference", "calf_circumference", "body_type",
// Lifestyle
"activity_level", "goal", "job_type", "sleep_hours", "sleep_quality",
"water_intake", "stress_level", "daily_steps",
// Workout
"workout_experience", "workout_days_per_week", "workout_location",
"workout_duration_minutes", "has_equipment", "available_equipment",
"fitness_goal", "cardio_preference",
// Injury & disability
"has_injury", "injury_area", "injury_severity", "injury_notes",
"has_disability", "disability_type", "disability_details", "mobility_level",
"uses_wheelchair", "has_prosthetic", "prosthetic_area",
// Medical
"health_conditions", "chronic_diseases", "medications", "surgeries",
"blood_type", "blood_pressure", "blood_sugar", "cholesterol_level",
"heart_condition", "heart_condition_details",
"has_diabetes", "diabetes_type", "has_thyroid", "thyroid_type",
"has_asthma", "has_epilepsy", "has_arthritis", "arthritis_type",
"has_osteoporosis", "has_fibromyalgia", "mental_health",
// Nutrition
"meals_per_day", "allergies", "dietary_restrictions",
"food_intolerances", "supplement_use", "caffeine_intake",
"smoking", "alcohol",
];
function validateEnum(value, validValues, fieldName) {
if (value !== undefined && value !== null && value !== "" && !validValues.includes(value)) {
return `Invalid ${fieldName} value`;
}
return null;
}
function validateNumber(value, min, max, fieldName) {
if (value !== undefined && value !== null && value !== "") {
const num = Number(value);
if (isNaN(num)) return `${fieldName} must be a number`;
if (min !== null && num < min) return `${fieldName} minimum is ${min}`;
if (max !== null && num > max) return `${fieldName} maximum is ${max}`;
}
return null;
}
function validate(body) {
const checks = [
// Required
validateNumber(body.age, 10, 120, "Age"),
validateEnum(body.gender, VALID_GENDERS, "gender"),
validateNumber(body.height, 50, 300, "Height"),
validateNumber(body.weight, 20, 500, "Weight"),
// Optional enums
validateEnum(body.activity_level, VALID_ACTIVITY_LEVELS, "activity_level"),
validateEnum(body.goal, VALID_GOALS, "goal"),
validateEnum(body.body_type, VALID_BODY_TYPES, "body_type"),
validateEnum(body.stress_level, VALID_STRESS_LEVELS, "stress_level"),
validateEnum(body.sleep_quality, VALID_SLEEP_QUALITY, "sleep_quality"),
validateEnum(body.job_type, VALID_JOB_TYPES, "job_type"),
validateEnum(body.workout_experience, VALID_WORKOUT_EXPERIENCE, "workout_experience"),
validateEnum(body.workout_location, VALID_WORKOUT_LOCATIONS, "workout_location"),
validateEnum(body.fitness_goal, VALID_FITNESS_GOALS, "fitness_goal"),
validateEnum(body.cardio_preference, VALID_CARDIO_PREFERENCES, "cardio_preference"),
validateEnum(body.injury_area, VALID_INJURY_AREAS, "injury_area"),
validateEnum(body.injury_severity, VALID_INJURY_SEVERITY, "injury_severity"),
validateEnum(body.disability_type, VALID_DISABILITY_TYPES, "disability_type"),
validateEnum(body.mobility_level, VALID_MOBILITY_LEVELS, "mobility_level"),
validateEnum(body.blood_type, VALID_BLOOD_TYPES, "blood_type"),
validateEnum(body.blood_pressure, VALID_BP_LEVELS, "blood_pressure"),
validateEnum(body.blood_sugar, VALID_SUGAR_LEVELS, "blood_sugar"),
validateEnum(body.cholesterol_level, VALID_CHOLESTEROL_LEVELS, "cholesterol_level"),
validateEnum(body.diabetes_type, VALID_DIABETES_TYPES, "diabetes_type"),
validateEnum(body.thyroid_type, VALID_THYROID_TYPES, "thyroid_type"),
validateEnum(body.arthritis_type, VALID_ARTHRITIS_TYPES, "arthritis_type"),
validateEnum(body.mental_health, VALID_MENTAL_HEALTH, "mental_health"),
validateEnum(body.caffeine_intake, VALID_CAFFEINE, "caffeine_intake"),
validateEnum(body.smoking, VALID_SMOKING, "smoking"),
validateEnum(body.alcohol, VALID_ALCOHOL, "alcohol"),
// Optional numbers
validateNumber(body.target_weight, 20, 500, "Target weight"),
validateNumber(body.body_fat_percentage, 2, 60, "Body fat"),
validateNumber(body.waist_circumference, 30, 200, "Waist"),
validateNumber(body.hip_circumference, 40, 200, "Hip"),
validateNumber(body.neck_circumference, 20, 60, "Neck"),
validateNumber(body.wrist_circumference, 10, 30, "Wrist"),
validateNumber(body.chest_circumference, 50, 200, "Chest"),
validateNumber(body.arm_circumference, 15, 70, "Arm"),
validateNumber(body.thigh_circumference, 20, 100, "Thigh"),
validateNumber(body.calf_circumference, 15, 70, "Calf"),
validateNumber(body.sleep_hours, 0, 24, "Sleep hours"),
validateNumber(body.water_intake, 0, 20, "Water intake"),
validateNumber(body.workout_days_per_week, 0, 7, "Workout days"),
validateNumber(body.workout_duration_minutes, 10, 240, "Workout duration"),
validateNumber(body.meals_per_day, 1, 10, "Meals per day"),
validateNumber(body.daily_steps, 0, 100000, "Daily steps"),
];
return checks.find((e) => e !== null) || null;
}
function toNum(v) {
return v != null && v !== "" ? Number(v) : null;
}
function toBool(v) {
return v ? 1 : 0;
}
// GET /api/profile // GET /api/profile
router.get("/", authenticateToken, (req, res) => { router.get("/", authenticateToken, (req, res) => {
try { try {
const db = getDb(); const db = getDb();
const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id); const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id);
res.json({ profile: profile || null });
if (!profile) {
return res.json({ profile: null });
}
res.json({ profile });
} catch (err) { } catch (err) {
console.error("Get profile error:", err.message); console.error("Get profile error:", err.message);
res.status(500).json({ error: "Sunucu hatası" }); res.status(500).json({ error: "Server error" });
} }
}); });
// POST /api/profile // POST /api/profile
router.post("/", authenticateToken, (req, res) => { router.post("/", authenticateToken, (req, res) => {
try { try {
const { age, gender, height, weight, activity_level, country, city, goal, allergies, dietary_restrictions } = req.body; const b = req.body;
if (!age || !gender || !height || !weight) { if (!b.age || !b.gender || !b.height || !b.weight) {
return res.status(400).json({ error: "Yaş, cinsiyet, boy ve kilo gereklidir" }); return res.status(400).json({ error: "Age, gender, height and weight are required" });
} }
const validGenders = ["male", "female"]; const err = validate(b);
const validActivityLevels = ["sedentary", "light", "moderate", "active", "very_active"]; if (err) return res.status(400).json({ error: err });
const validGoals = ["lose_weight", "gain_weight", "build_muscle", "maintain"];
if (!validGenders.includes(gender)) {
return res.status(400).json({ error: "Geçersiz cinsiyet değeri" });
}
if (activity_level && !validActivityLevels.includes(activity_level)) {
return res.status(400).json({ error: "Geçersiz aktivite seviyesi" });
}
if (goal && !validGoals.includes(goal)) {
return res.status(400).json({ error: "Geçersiz hedef" });
}
const db = getDb(); const db = getDb();
const existing = db.prepare("SELECT user_id FROM profiles WHERE user_id = ?").get(req.user.id); const existing = db.prepare("SELECT user_id FROM profiles WHERE user_id = ?").get(req.user.id);
const values = {
// Basic
age: Number(b.age),
gender: b.gender,
height: Number(b.height),
weight: Number(b.weight),
target_weight: toNum(b.target_weight),
// Body composition
body_fat_percentage: toNum(b.body_fat_percentage),
waist_circumference: toNum(b.waist_circumference),
hip_circumference: toNum(b.hip_circumference),
neck_circumference: toNum(b.neck_circumference),
wrist_circumference: toNum(b.wrist_circumference),
chest_circumference: toNum(b.chest_circumference),
arm_circumference: toNum(b.arm_circumference),
thigh_circumference: toNum(b.thigh_circumference),
calf_circumference: toNum(b.calf_circumference),
body_type: b.body_type || null,
// Lifestyle
activity_level: b.activity_level || "moderate",
goal: b.goal || "maintain",
job_type: b.job_type || null,
sleep_hours: toNum(b.sleep_hours),
sleep_quality: b.sleep_quality || "moderate",
water_intake: toNum(b.water_intake),
stress_level: b.stress_level || "medium",
daily_steps: toNum(b.daily_steps),
// Workout
workout_experience: b.workout_experience || "beginner",
workout_days_per_week: toNum(b.workout_days_per_week) ?? 3,
workout_location: b.workout_location || "gym",
workout_duration_minutes: toNum(b.workout_duration_minutes) ?? 60,
has_equipment: toBool(b.has_equipment),
available_equipment: b.available_equipment || "",
fitness_goal: b.fitness_goal || "general_fitness",
cardio_preference: b.cardio_preference || "moderate",
// Injury & disability
has_injury: toBool(b.has_injury),
injury_area: b.injury_area || "none",
injury_severity: b.injury_severity || null,
injury_notes: b.injury_notes || null,
has_disability: toBool(b.has_disability),
disability_type: b.disability_type || "none",
disability_details: b.disability_details || null,
mobility_level: b.mobility_level || "full",
uses_wheelchair: toBool(b.uses_wheelchair),
has_prosthetic: toBool(b.has_prosthetic),
prosthetic_area: b.prosthetic_area || null,
// Medical
health_conditions: b.health_conditions || "",
chronic_diseases: b.chronic_diseases || "",
medications: b.medications || "",
surgeries: b.surgeries || "",
blood_type: b.blood_type || null,
blood_pressure: b.blood_pressure || "normal",
blood_sugar: b.blood_sugar || "normal",
cholesterol_level: b.cholesterol_level || "normal",
heart_condition: toBool(b.heart_condition),
heart_condition_details: b.heart_condition_details || null,
has_diabetes: toBool(b.has_diabetes),
diabetes_type: b.diabetes_type || null,
has_thyroid: toBool(b.has_thyroid),
thyroid_type: b.thyroid_type || null,
has_asthma: toBool(b.has_asthma),
has_epilepsy: toBool(b.has_epilepsy),
has_arthritis: toBool(b.has_arthritis),
arthritis_type: b.arthritis_type || null,
has_osteoporosis: toBool(b.has_osteoporosis),
has_fibromyalgia: toBool(b.has_fibromyalgia),
mental_health: b.mental_health || "none",
// Nutrition
meals_per_day: toNum(b.meals_per_day) ?? 4,
allergies: b.allergies || "",
dietary_restrictions: b.dietary_restrictions || "",
food_intolerances: b.food_intolerances || "",
supplement_use: b.supplement_use || "",
caffeine_intake: b.caffeine_intake || "moderate",
smoking: b.smoking || "no",
alcohol: b.alcohol || "none",
};
if (existing) { if (existing) {
db.prepare(` const setClauses = PROFILE_FIELDS.map((f) => `${f}=@${f}`).join(", ");
UPDATE profiles SET age=?, gender=?, height=?, weight=?, activity_level=?, country=?, city=?, goal=?, allergies=?, dietary_restrictions=?, updated_at=datetime('now') db.prepare(
WHERE user_id=? `UPDATE profiles SET ${setClauses}, updated_at=datetime('now') WHERE user_id=@user_id`
`).run(age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || "", req.user.id); ).run({ ...values, user_id: req.user.id });
} else { } else {
db.prepare(` const cols = ["user_id", ...PROFILE_FIELDS].join(", ");
INSERT INTO profiles (user_id, age, gender, height, weight, activity_level, country, city, goal, allergies, dietary_restrictions) const placeholders = ["@user_id", ...PROFILE_FIELDS.map((f) => `@${f}`)].join(", ");
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) db.prepare(
`).run(req.user.id, age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || ""); `INSERT INTO profiles (${cols}) VALUES (${placeholders})`
).run({ ...values, user_id: req.user.id });
} }
const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id); const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id);
res.json({ message: "Profil güncellendi", profile }); res.json({ message: "Profile updated", profile });
} catch (err) { } catch (err) {
console.error("Save profile error:", err.message); console.error("Save profile error:", err.message);
res.status(500).json({ error: "Sunucu hatası" }); res.status(500).json({ error: "Server error" });
} }
}); });

View File

@@ -4,7 +4,7 @@ const express = require("express");
const { getDb } = require("../database"); const { getDb } = require("../database");
const { authenticateToken } = require("../middleware/auth"); const { authenticateToken } = require("../middleware/auth");
const { generateWorkoutProgram } = require("../services/trainer"); const { generateWorkoutProgram } = require("../services/trainer");
const { generateMealPlan } = require("../services/dietitian"); const { generateDietPlan } = require("../services/dietitian");
const { generateShoppingList } = require("../services/shopping"); const { generateShoppingList } = require("../services/shopping");
const router = express.Router(); const router = express.Router();
@@ -14,15 +14,20 @@ function getProfile(userId) {
return db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(userId); return db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(userId);
} }
// GET /api/program/workout // GET /api/program/workout?week=1
router.get("/workout", authenticateToken, (req, res) => { router.get("/workout", authenticateToken, (req, res) => {
try { try {
const profile = getProfile(req.user.id); const profile = getProfile(req.user.id);
if (!profile) { if (!profile) {
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
} }
const program = generateWorkoutProgram(profile); let week = parseInt(req.query.week, 10);
res.json(program); if (isNaN(week)) week = 1;
if (week < 1 || week > 4) {
return res.status(400).json({ error: "Hafta 1-4 arasında olmalıdır" });
}
const program = generateWorkoutProgram(profile, week);
res.json({ workout: program });
} catch (err) { } catch (err) {
console.error("Workout program error:", err.message); console.error("Workout program error:", err.message);
res.status(500).json({ error: "Program oluşturulurken hata oluştu" }); res.status(500).json({ error: "Program oluşturulurken hata oluştu" });
@@ -36,8 +41,8 @@ router.get("/meal", authenticateToken, (req, res) => {
if (!profile) { if (!profile) {
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
} }
const plan = generateMealPlan(profile); const plan = generateDietPlan(profile);
res.json(plan); res.json({ meal_plan: plan });
} catch (err) { } catch (err) {
console.error("Meal plan error:", err.message); console.error("Meal plan error:", err.message);
res.status(500).json({ error: "Beslenme planı oluşturulurken hata oluştu" }); res.status(500).json({ error: "Beslenme planı oluşturulurken hata oluştu" });
@@ -51,9 +56,9 @@ router.get("/shopping", authenticateToken, (req, res) => {
if (!profile) { if (!profile) {
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
} }
const mealPlan = generateMealPlan(profile); const mealPlan = generateDietPlan(profile);
const shoppingList = generateShoppingList(mealPlan); const shoppingList = generateShoppingList(mealPlan);
res.json(shoppingList); res.json({ shopping: shoppingList });
} catch (err) { } catch (err) {
console.error("Shopping list error:", err.message); console.error("Shopping list error:", err.message);
res.status(500).json({ error: "Alışveriş listesi oluşturulurken hata oluştu" }); res.status(500).json({ error: "Alışveriş listesi oluşturulurken hata oluştu" });

File diff suppressed because it is too large Load Diff

View File

@@ -2,83 +2,75 @@
const CATEGORY_MAP = { const CATEGORY_MAP = {
// Protein // Protein
"yumurta": "protein", "eggs": "protein", "egg": "protein", "egg": "protein", "eggs": "protein", "chicken": "protein", "chicken breast": "protein",
"tavuk": "protein", "chicken": "protein", "turkey breast": "protein", "turkey": "protein", "turkey breast": "protein", "ground turkey": "protein",
"dana": "protein", "beef": "protein", "steak": "protein", "lean steak": "protein", "beef": "protein", "lean beef": "protein", "ground beef": "protein", "steak": "protein",
"kıyma": "protein", "ground turkey": "protein", "ground beef": "protein", "salmon": "protein", "tuna": "protein", "cod": "protein", "shrimp": "protein",
"sucuk": "protein", "köfte": "protein", "kebap": "protein", "adana": "protein", "fish": "protein", "white fish": "protein", "tofu": "protein", "tempeh": "protein",
"balık": "protein", "levrek": "protein", "çipura": "protein", "salmon": "protein", "protein powder": "protein", "whey protein": "protein",
"cod": "protein", "tuna": "protein", "shrimp": "protein", "lamb": "protein", "pork": "protein",
"et ": "protein", "kuşbaşı": "protein", "mantı": "protein",
"lahmacun": "protein", "pide hamuru": "protein",
"protein powder": "protein", "protein bar": "protein",
// Dairy // Dairy
"peynir": "dairy", "cheese": "dairy", "feta": "dairy", "parmesan": "dairy", "swiss": "dairy", "cheese": "dairy", "feta": "dairy", "parmesan": "dairy", "mozzarella": "dairy",
"süt": "dairy", "milk": "dairy", "almond milk": "dairy", "cheddar": "dairy", "swiss cheese": "dairy", "cottage cheese": "dairy",
"yoğurt": "dairy", "yogurt": "dairy", "greek yogurt": "dairy", "cottage cheese": "dairy", "milk": "dairy", "almond milk": "dairy", "oat milk": "dairy", "soy milk": "dairy",
"kaymak": "dairy", "tereyağı": "dairy", "butter": "dairy", "yogurt": "dairy", "greek yogurt": "dairy", "butter": "dairy", "cream": "dairy",
"ayran": "dairy", "lor": "dairy", "kaşar": "dairy", "cream cheese": "dairy",
"cream": "dairy",
// Vegetables // Vegetables
"domates": "vegetables", "tomato": "vegetables", "cherry tomato": "vegetables", "tomato": "vegetables", "cherry tomato": "vegetables", "tomatoes": "vegetables",
"salatalık": "vegetables", "cucumber": "vegetables", "cucumber": "vegetables", "bell pepper": "vegetables", "pepper": "vegetables",
"biber": "vegetables", "bell pepper": "vegetables", "pepper": "vegetables", "onion": "vegetables", "garlic": "vegetables", "carrot": "vegetables",
"soğan": "vegetables", "onion": "vegetables", "zucchini": "vegetables", "spinach": "vegetables", "kale": "vegetables",
"sarımsak": "vegetables", "garlic": "vegetables", "lettuce": "vegetables", "mixed greens": "vegetables", "arugula": "vegetables",
"patlıcan": "vegetables", "havuç": "vegetables", "carrot": "vegetables", "asparagus": "vegetables", "broccoli": "vegetables", "cauliflower": "vegetables",
"kabak": "vegetables", "zucchini": "vegetables", "celery": "vegetables", "mushroom": "vegetables", "mushrooms": "vegetables",
"ıspanak": "vegetables", "ispanak": "vegetables", "spinach": "vegetables", "sweet potato": "vegetables", "potato": "vegetables", "corn": "vegetables",
"marul": "vegetables", "lettuce": "vegetables", "mixed greens": "vegetables", "eggplant": "vegetables", "cabbage": "vegetables", "brussels sprouts": "vegetables",
"roka": "vegetables", "asparagus": "vegetables", "broccoli": "vegetables", "green beans": "vegetables", "peas": "vegetables",
"fasulye": "vegetables", "bamya": "vegetables", "taze fasulye": "vegetables",
"maydanoz": "vegetables", "dereotu": "vegetables", "nane": "vegetables",
"asma yaprağı": "vegetables", "celery": "vegetables",
"roasted vegetables": "vegetables", "mixed vegetables": "vegetables", "roasted vegetables": "vegetables", "mixed vegetables": "vegetables",
"sweet potato": "vegetables", "baked potato": "vegetables", "potato": "vegetables", "edamame": "vegetables",
// Fruits // Fruits
"muz": "fruits", "banana": "fruits", "banana": "fruits", "apple": "fruits", "berries": "fruits", "mixed berries": "fruits",
"elma": "fruits", "apple": "fruits", "blueberries": "fruits", "strawberries": "fruits", "raspberries": "fruits",
"meyve": "fruits", "berries": "fruits", "mixed berries": "fruits", "orange": "fruits", "lemon": "fruits", "lime": "fruits", "avocado": "fruits",
"limon": "fruits", "lemon": "fruits", "mango": "fruits", "pineapple": "fruits", "grapes": "fruits", "pear": "fruits",
"nar ekşisi": "fruits", "cranberries": "fruits", "peach": "fruits", "watermelon": "fruits", "dried fruits": "fruits",
"hurma": "fruits", "kuru kayısı": "fruits", "cranberries": "fruits", "dates": "fruits",
// Grains // Grains
"bulgur": "grains", "pirinç": "grains", "pilav": "grains", "rice": "grains", "brown rice": "grains", "white rice": "grains",
"rice": "grains", "brown rice": "grains", "quinoa": "grains", "quinoa": "grains", "oats": "grains", "rolled oats": "grains", "granola": "grains",
"ekmek": "grains", "bread": "grains", "whole wheat": "grains", "toast": "grains", "bread": "grains", "whole wheat bread": "grains", "sourdough": "grains",
"simit": "grains", "lavaş": "grains", "tortilla": "grains", "tortilla": "grains", "wrap": "grains", "pita": "grains",
"un": "grains", "yufka": "grains", "pasta": "grains", "whole wheat pasta": "grains", "noodles": "grains",
"mercimek": "grains", "kuru fasulye": "grains", "nohut": "grains", "couscous": "grains", "bulgur": "grains",
"oats": "grains", "granola": "grains", "pasta": "grains", "lentils": "grains", "chickpeas": "grains", "black beans": "grains",
"chia": "grains", "kraker": "grains", "kidney beans": "grains", "beans": "grains",
"chia seeds": "grains", "flax seeds": "grains",
// Spices & Condiments
"pul biber": "spices", "tuz": "spices", "karabiber": "spices",
"sumak": "spices", "herbs": "spices", "red pepper": "spices",
"salça": "spices", "domates salçası": "spices",
"soy sauce": "spices", "sesame oil": "spices", "marinara": "spices",
"honey": "spices", "bal": "spices",
// Fats & Nuts // Fats & Nuts
"zeytinyağı": "fats_nuts", "olive oil": "fats_nuts", "olive oil": "fats_nuts", "coconut oil": "fats_nuts", "sesame oil": "fats_nuts",
"zeytin": "fats_nuts", "almonds": "fats_nuts", "walnuts": "fats_nuts", "cashews": "fats_nuts",
"ceviz": "fats_nuts", "fındık": "fats_nuts", "badem": "fats_nuts", "peanuts": "fats_nuts", "pecans": "fats_nuts", "pistachios": "fats_nuts",
"almonds": "fats_nuts", "peanut butter": "fats_nuts", "almond butter": "fats_nuts", "peanut butter": "fats_nuts", "almond butter": "fats_nuts",
"fıstık ezmesi": "fats_nuts", "avocado": "fats_nuts", "mixed nuts": "fats_nuts", "seeds": "fats_nuts", "sunflower seeds": "fats_nuts",
"mixed nuts": "fats_nuts", "dark chocolate": "fats_nuts", "pumpkin seeds": "fats_nuts", "tahini": "fats_nuts",
"humus": "fats_nuts", "dark chocolate": "fats_nuts", "hummus": "fats_nuts",
// Spices & Condiments
"salt": "spices", "pepper": "spices", "cumin": "spices", "paprika": "spices",
"turmeric": "spices", "cinnamon": "spices", "oregano": "spices", "basil": "spices",
"herbs": "spices", "mixed herbs": "spices", "fresh herbs": "spices",
"soy sauce": "spices", "honey": "spices", "maple syrup": "spices",
"vinegar": "spices", "balsamic vinegar": "spices", "mustard": "spices",
"hot sauce": "spices", "salsa": "spices", "marinara": "spices",
"tomato sauce": "spices", "pesto": "spices",
// Beverages // Beverages
"çay": "beverages", "kahve": "beverages", "türk kahvesi": "beverages", "water": "beverages", "green tea": "beverages", "herbal tea": "beverages",
"şalgam": "beverages", "coffee": "beverages", "coconut water": "beverages",
"su": "beverages", "water": "beverages",
// Other
"turşu": "other",
}; };
function categorizeIngredient(itemName) { function categorizeIngredient(itemName) {
@@ -94,49 +86,54 @@ function categorizeIngredient(itemName) {
function generateShoppingList(mealPlan) { function generateShoppingList(mealPlan) {
const ingredientMap = {}; const ingredientMap = {};
for (const day of mealPlan.days) { for (const day of mealPlan.days || []) {
for (const meal of day.meals) { for (const meal of day.meals || []) {
for (const ing of meal.ingredients) { for (const ing of meal.ingredients || []) {
const key = ing.item.toLowerCase().trim(); // Support both old format (item/grams) and new format (name/amount/unit)
const name = ing.name || ing.item || "unknown";
const amount = ing.amount || ing.grams || 0;
const unit = ing.unit || "g";
const key = name.toLowerCase().trim();
if (!ingredientMap[key]) { if (!ingredientMap[key]) {
ingredientMap[key] = { ingredientMap[key] = {
item: ing.item, item: name,
total_grams: 0, total_amount: 0,
category: categorizeIngredient(ing.item), unit,
category: categorizeIngredient(name),
}; };
} }
ingredientMap[key].total_grams += ing.grams; ingredientMap[key].total_amount += amount;
} }
} }
} }
const categories = { const categories = {
protein: { name: "Protein Kaynakları", icon: "🥩", items: [] }, protein: { name: "Protein Sources", icon: "🥩", items: [] },
dairy: { name: "Süt Ürünleri", icon: "🧀", items: [] }, dairy: { name: "Dairy", icon: "🧀", items: [] },
vegetables: { name: "Sebzeler", icon: "🥬", items: [] }, vegetables: { name: "Vegetables", icon: "🥬", items: [] },
fruits: { name: "Meyveler", icon: "🍎", items: [] }, fruits: { name: "Fruits", icon: "🍎", items: [] },
grains: { name: "Tahıllar & Baklagiller", icon: "🌾", items: [] }, grains: { name: "Grains & Legumes", icon: "🌾", items: [] },
fats_nuts: { name: "Yağlar & Kuruyemişler", icon: "🥜", items: [] }, fats_nuts: { name: "Fats & Nuts", icon: "🥜", items: [] },
spices: { name: "Baharat & Sos", icon: "🧂", items: [] }, spices: { name: "Spices & Condiments", icon: "🧂", items: [] },
beverages: { name: "İçecekler", icon: "🥤", items: [] }, beverages: { name: "Beverages", icon: "🥤", items: [] },
other: { name: "Diğer", icon: "📦", items: [] }, other: { name: "Other", icon: "📦", items: [] },
}; };
for (const data of Object.values(ingredientMap)) { for (const data of Object.values(ingredientMap)) {
const cat = categories[data.category] || categories.other; const cat = categories[data.category] || categories.other;
cat.items.push({ cat.items.push({
item: data.item, item: data.item,
total_grams: Math.round(data.total_grams), total_amount: Math.round(data.total_amount),
display_amount: formatAmount(data.total_grams), unit: data.unit,
display_amount: formatAmount(data.total_amount, data.unit),
}); });
} }
// Sort items within each category alphabetically
for (const cat of Object.values(categories)) { for (const cat of Object.values(categories)) {
cat.items.sort((a, b) => a.item.localeCompare(b.item, "tr")); cat.items.sort((a, b) => a.item.localeCompare(b.item));
} }
// Remove empty categories
const result = {}; const result = {};
for (const [key, cat] of Object.entries(categories)) { for (const [key, cat] of Object.entries(categories)) {
if (cat.items.length > 0) { if (cat.items.length > 0) {
@@ -145,18 +142,17 @@ function generateShoppingList(mealPlan) {
} }
return { return {
title: "Haftalık Alışveriş Listesi", title: "Weekly Shopping List",
note: "Bu liste 7 günlük beslenme programınıza göre hazırlanmıştır.", note: "This list is based on your 7-day meal plan.",
categories: result, categories: result,
total_items: Object.values(ingredientMap).length, total_items: Object.values(ingredientMap).length,
}; };
} }
function formatAmount(grams) { function formatAmount(amount, unit) {
if (grams >= 1000) { if (unit === "g" && amount >= 1000) return `${(amount / 1000).toFixed(1)} kg`;
return `${(grams / 1000).toFixed(1)} kg`; if (unit === "ml" && amount >= 1000) return `${(amount / 1000).toFixed(1)} L`;
} return `${Math.round(amount)} ${unit}`;
return `${grams}g`;
} }
module.exports = { generateShoppingList }; module.exports = { generateShoppingList };

File diff suppressed because it is too large Load Diff