feat: major update - medical profiles, global diet, gym/home/none workouts, exercise GIFs
- 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:
@@ -29,16 +29,85 @@ function initTables() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS profiles (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
-- Basic metrics
|
||||
age INTEGER,
|
||||
gender TEXT,
|
||||
height 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',
|
||||
country TEXT DEFAULT 'Turkey',
|
||||
city TEXT DEFAULT '',
|
||||
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 '',
|
||||
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')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,66 +6,270 @@ const { authenticateToken } = require("../middleware/auth");
|
||||
|
||||
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
|
||||
router.get("/", authenticateToken, (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id);
|
||||
|
||||
if (!profile) {
|
||||
return res.json({ profile: null });
|
||||
}
|
||||
|
||||
res.json({ profile });
|
||||
res.json({ profile: profile || null });
|
||||
} catch (err) {
|
||||
console.error("Get profile error:", err.message);
|
||||
res.status(500).json({ error: "Sunucu hatası" });
|
||||
res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/profile
|
||||
router.post("/", authenticateToken, (req, res) => {
|
||||
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) {
|
||||
return res.status(400).json({ error: "Yaş, cinsiyet, boy ve kilo gereklidir" });
|
||||
if (!b.age || !b.gender || !b.height || !b.weight) {
|
||||
return res.status(400).json({ error: "Age, gender, height and weight are required" });
|
||||
}
|
||||
|
||||
const validGenders = ["male", "female"];
|
||||
const validActivityLevels = ["sedentary", "light", "moderate", "active", "very_active"];
|
||||
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 err = validate(b);
|
||||
if (err) return res.status(400).json({ error: err });
|
||||
|
||||
const db = getDb();
|
||||
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) {
|
||||
db.prepare(`
|
||||
UPDATE profiles SET age=?, gender=?, height=?, weight=?, activity_level=?, country=?, city=?, goal=?, allergies=?, dietary_restrictions=?, updated_at=datetime('now')
|
||||
WHERE user_id=?
|
||||
`).run(age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || "", req.user.id);
|
||||
const setClauses = PROFILE_FIELDS.map((f) => `${f}=@${f}`).join(", ");
|
||||
db.prepare(
|
||||
`UPDATE profiles SET ${setClauses}, updated_at=datetime('now') WHERE user_id=@user_id`
|
||||
).run({ ...values, user_id: req.user.id });
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO profiles (user_id, age, gender, height, weight, activity_level, country, city, goal, allergies, dietary_restrictions)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(req.user.id, age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || "");
|
||||
const cols = ["user_id", ...PROFILE_FIELDS].join(", ");
|
||||
const placeholders = ["@user_id", ...PROFILE_FIELDS.map((f) => `@${f}`)].join(", ");
|
||||
db.prepare(
|
||||
`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);
|
||||
res.json({ message: "Profil güncellendi", profile });
|
||||
res.json({ message: "Profile updated", profile });
|
||||
} catch (err) {
|
||||
console.error("Save profile error:", err.message);
|
||||
res.status(500).json({ error: "Sunucu hatası" });
|
||||
res.status(500).json({ error: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const express = require("express");
|
||||
const { getDb } = require("../database");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const { generateWorkoutProgram } = require("../services/trainer");
|
||||
const { generateMealPlan } = require("../services/dietitian");
|
||||
const { generateDietPlan } = require("../services/dietitian");
|
||||
const { generateShoppingList } = require("../services/shopping");
|
||||
|
||||
const router = express.Router();
|
||||
@@ -14,15 +14,20 @@ function getProfile(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) => {
|
||||
try {
|
||||
const profile = getProfile(req.user.id);
|
||||
if (!profile) {
|
||||
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
|
||||
}
|
||||
const program = generateWorkoutProgram(profile);
|
||||
res.json(program);
|
||||
let week = parseInt(req.query.week, 10);
|
||||
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) {
|
||||
console.error("Workout program error:", err.message);
|
||||
res.status(500).json({ error: "Program oluşturulurken hata oluştu" });
|
||||
@@ -36,8 +41,8 @@ router.get("/meal", authenticateToken, (req, res) => {
|
||||
if (!profile) {
|
||||
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
|
||||
}
|
||||
const plan = generateMealPlan(profile);
|
||||
res.json(plan);
|
||||
const plan = generateDietPlan(profile);
|
||||
res.json({ meal_plan: plan });
|
||||
} catch (err) {
|
||||
console.error("Meal plan error:", err.message);
|
||||
res.status(500).json({ error: "Beslenme planı oluşturulurken hata oluştu" });
|
||||
@@ -51,9 +56,9 @@ router.get("/shopping", authenticateToken, (req, res) => {
|
||||
if (!profile) {
|
||||
return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" });
|
||||
}
|
||||
const mealPlan = generateMealPlan(profile);
|
||||
const mealPlan = generateDietPlan(profile);
|
||||
const shoppingList = generateShoppingList(mealPlan);
|
||||
res.json(shoppingList);
|
||||
res.json({ shopping: shoppingList });
|
||||
} catch (err) {
|
||||
console.error("Shopping list error:", err.message);
|
||||
res.status(500).json({ error: "Alışveriş listesi oluşturulurken hata oluştu" });
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,83 +2,75 @@
|
||||
|
||||
const CATEGORY_MAP = {
|
||||
// Protein
|
||||
"yumurta": "protein", "eggs": "protein", "egg": "protein",
|
||||
"tavuk": "protein", "chicken": "protein", "turkey breast": "protein",
|
||||
"dana": "protein", "beef": "protein", "steak": "protein", "lean steak": "protein",
|
||||
"kıyma": "protein", "ground turkey": "protein", "ground beef": "protein",
|
||||
"sucuk": "protein", "köfte": "protein", "kebap": "protein", "adana": "protein",
|
||||
"balık": "protein", "levrek": "protein", "çipura": "protein", "salmon": "protein",
|
||||
"cod": "protein", "tuna": "protein", "shrimp": "protein",
|
||||
"et ": "protein", "kuşbaşı": "protein", "mantı": "protein",
|
||||
"lahmacun": "protein", "pide hamuru": "protein",
|
||||
"protein powder": "protein", "protein bar": "protein",
|
||||
"egg": "protein", "eggs": "protein", "chicken": "protein", "chicken breast": "protein",
|
||||
"turkey": "protein", "turkey breast": "protein", "ground turkey": "protein",
|
||||
"beef": "protein", "lean beef": "protein", "ground beef": "protein", "steak": "protein",
|
||||
"salmon": "protein", "tuna": "protein", "cod": "protein", "shrimp": "protein",
|
||||
"fish": "protein", "white fish": "protein", "tofu": "protein", "tempeh": "protein",
|
||||
"protein powder": "protein", "whey protein": "protein",
|
||||
"lamb": "protein", "pork": "protein",
|
||||
|
||||
// Dairy
|
||||
"peynir": "dairy", "cheese": "dairy", "feta": "dairy", "parmesan": "dairy", "swiss": "dairy",
|
||||
"süt": "dairy", "milk": "dairy", "almond milk": "dairy",
|
||||
"yoğurt": "dairy", "yogurt": "dairy", "greek yogurt": "dairy", "cottage cheese": "dairy",
|
||||
"kaymak": "dairy", "tereyağı": "dairy", "butter": "dairy",
|
||||
"ayran": "dairy", "lor": "dairy", "kaşar": "dairy",
|
||||
"cream": "dairy",
|
||||
"cheese": "dairy", "feta": "dairy", "parmesan": "dairy", "mozzarella": "dairy",
|
||||
"cheddar": "dairy", "swiss cheese": "dairy", "cottage cheese": "dairy",
|
||||
"milk": "dairy", "almond milk": "dairy", "oat milk": "dairy", "soy milk": "dairy",
|
||||
"yogurt": "dairy", "greek yogurt": "dairy", "butter": "dairy", "cream": "dairy",
|
||||
"cream cheese": "dairy",
|
||||
|
||||
// Vegetables
|
||||
"domates": "vegetables", "tomato": "vegetables", "cherry tomato": "vegetables",
|
||||
"salatalık": "vegetables", "cucumber": "vegetables",
|
||||
"biber": "vegetables", "bell pepper": "vegetables", "pepper": "vegetables",
|
||||
"soğan": "vegetables", "onion": "vegetables",
|
||||
"sarımsak": "vegetables", "garlic": "vegetables",
|
||||
"patlıcan": "vegetables", "havuç": "vegetables", "carrot": "vegetables",
|
||||
"kabak": "vegetables", "zucchini": "vegetables",
|
||||
"ıspanak": "vegetables", "ispanak": "vegetables", "spinach": "vegetables",
|
||||
"marul": "vegetables", "lettuce": "vegetables", "mixed greens": "vegetables",
|
||||
"roka": "vegetables", "asparagus": "vegetables", "broccoli": "vegetables",
|
||||
"fasulye": "vegetables", "bamya": "vegetables", "taze fasulye": "vegetables",
|
||||
"maydanoz": "vegetables", "dereotu": "vegetables", "nane": "vegetables",
|
||||
"asma yaprağı": "vegetables", "celery": "vegetables",
|
||||
"tomato": "vegetables", "cherry tomato": "vegetables", "tomatoes": "vegetables",
|
||||
"cucumber": "vegetables", "bell pepper": "vegetables", "pepper": "vegetables",
|
||||
"onion": "vegetables", "garlic": "vegetables", "carrot": "vegetables",
|
||||
"zucchini": "vegetables", "spinach": "vegetables", "kale": "vegetables",
|
||||
"lettuce": "vegetables", "mixed greens": "vegetables", "arugula": "vegetables",
|
||||
"asparagus": "vegetables", "broccoli": "vegetables", "cauliflower": "vegetables",
|
||||
"celery": "vegetables", "mushroom": "vegetables", "mushrooms": "vegetables",
|
||||
"sweet potato": "vegetables", "potato": "vegetables", "corn": "vegetables",
|
||||
"eggplant": "vegetables", "cabbage": "vegetables", "brussels sprouts": "vegetables",
|
||||
"green beans": "vegetables", "peas": "vegetables",
|
||||
"roasted vegetables": "vegetables", "mixed vegetables": "vegetables",
|
||||
"sweet potato": "vegetables", "baked potato": "vegetables", "potato": "vegetables",
|
||||
"edamame": "vegetables",
|
||||
|
||||
// Fruits
|
||||
"muz": "fruits", "banana": "fruits",
|
||||
"elma": "fruits", "apple": "fruits",
|
||||
"meyve": "fruits", "berries": "fruits", "mixed berries": "fruits",
|
||||
"limon": "fruits", "lemon": "fruits",
|
||||
"nar ekşisi": "fruits", "cranberries": "fruits",
|
||||
"hurma": "fruits", "kuru kayısı": "fruits",
|
||||
"banana": "fruits", "apple": "fruits", "berries": "fruits", "mixed berries": "fruits",
|
||||
"blueberries": "fruits", "strawberries": "fruits", "raspberries": "fruits",
|
||||
"orange": "fruits", "lemon": "fruits", "lime": "fruits", "avocado": "fruits",
|
||||
"mango": "fruits", "pineapple": "fruits", "grapes": "fruits", "pear": "fruits",
|
||||
"peach": "fruits", "watermelon": "fruits", "dried fruits": "fruits",
|
||||
"cranberries": "fruits", "dates": "fruits",
|
||||
|
||||
// Grains
|
||||
"bulgur": "grains", "pirinç": "grains", "pilav": "grains",
|
||||
"rice": "grains", "brown rice": "grains", "quinoa": "grains",
|
||||
"ekmek": "grains", "bread": "grains", "whole wheat": "grains", "toast": "grains",
|
||||
"simit": "grains", "lavaş": "grains", "tortilla": "grains",
|
||||
"un": "grains", "yufka": "grains",
|
||||
"mercimek": "grains", "kuru fasulye": "grains", "nohut": "grains",
|
||||
"oats": "grains", "granola": "grains", "pasta": "grains",
|
||||
"chia": "grains", "kraker": "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",
|
||||
"rice": "grains", "brown rice": "grains", "white rice": "grains",
|
||||
"quinoa": "grains", "oats": "grains", "rolled oats": "grains", "granola": "grains",
|
||||
"bread": "grains", "whole wheat bread": "grains", "sourdough": "grains",
|
||||
"tortilla": "grains", "wrap": "grains", "pita": "grains",
|
||||
"pasta": "grains", "whole wheat pasta": "grains", "noodles": "grains",
|
||||
"couscous": "grains", "bulgur": "grains",
|
||||
"lentils": "grains", "chickpeas": "grains", "black beans": "grains",
|
||||
"kidney beans": "grains", "beans": "grains",
|
||||
"chia seeds": "grains", "flax seeds": "grains",
|
||||
|
||||
// Fats & Nuts
|
||||
"zeytinyağı": "fats_nuts", "olive oil": "fats_nuts",
|
||||
"zeytin": "fats_nuts",
|
||||
"ceviz": "fats_nuts", "fındık": "fats_nuts", "badem": "fats_nuts",
|
||||
"almonds": "fats_nuts", "peanut butter": "fats_nuts", "almond butter": "fats_nuts",
|
||||
"fıstık ezmesi": "fats_nuts", "avocado": "fats_nuts",
|
||||
"mixed nuts": "fats_nuts", "dark chocolate": "fats_nuts",
|
||||
"humus": "fats_nuts",
|
||||
"olive oil": "fats_nuts", "coconut oil": "fats_nuts", "sesame oil": "fats_nuts",
|
||||
"almonds": "fats_nuts", "walnuts": "fats_nuts", "cashews": "fats_nuts",
|
||||
"peanuts": "fats_nuts", "pecans": "fats_nuts", "pistachios": "fats_nuts",
|
||||
"peanut butter": "fats_nuts", "almond butter": "fats_nuts",
|
||||
"mixed nuts": "fats_nuts", "seeds": "fats_nuts", "sunflower seeds": "fats_nuts",
|
||||
"pumpkin seeds": "fats_nuts", "tahini": "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
|
||||
"çay": "beverages", "kahve": "beverages", "türk kahvesi": "beverages",
|
||||
"şalgam": "beverages",
|
||||
"su": "beverages", "water": "beverages",
|
||||
|
||||
// Other
|
||||
"turşu": "other",
|
||||
"water": "beverages", "green tea": "beverages", "herbal tea": "beverages",
|
||||
"coffee": "beverages", "coconut water": "beverages",
|
||||
};
|
||||
|
||||
function categorizeIngredient(itemName) {
|
||||
@@ -94,49 +86,54 @@ function categorizeIngredient(itemName) {
|
||||
function generateShoppingList(mealPlan) {
|
||||
const ingredientMap = {};
|
||||
|
||||
for (const day of mealPlan.days) {
|
||||
for (const meal of day.meals) {
|
||||
for (const ing of meal.ingredients) {
|
||||
const key = ing.item.toLowerCase().trim();
|
||||
for (const day of mealPlan.days || []) {
|
||||
for (const meal of day.meals || []) {
|
||||
for (const ing of meal.ingredients || []) {
|
||||
// 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]) {
|
||||
ingredientMap[key] = {
|
||||
item: ing.item,
|
||||
total_grams: 0,
|
||||
category: categorizeIngredient(ing.item),
|
||||
item: name,
|
||||
total_amount: 0,
|
||||
unit,
|
||||
category: categorizeIngredient(name),
|
||||
};
|
||||
}
|
||||
ingredientMap[key].total_grams += ing.grams;
|
||||
ingredientMap[key].total_amount += amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const categories = {
|
||||
protein: { name: "Protein Kaynakları", icon: "🥩", items: [] },
|
||||
dairy: { name: "Süt Ürünleri", icon: "🧀", items: [] },
|
||||
vegetables: { name: "Sebzeler", icon: "🥬", items: [] },
|
||||
fruits: { name: "Meyveler", icon: "🍎", items: [] },
|
||||
grains: { name: "Tahıllar & Baklagiller", icon: "🌾", items: [] },
|
||||
fats_nuts: { name: "Yağlar & Kuruyemişler", icon: "🥜", items: [] },
|
||||
spices: { name: "Baharat & Sos", icon: "🧂", items: [] },
|
||||
beverages: { name: "İçecekler", icon: "🥤", items: [] },
|
||||
other: { name: "Diğer", icon: "📦", items: [] },
|
||||
protein: { name: "Protein Sources", icon: "🥩", items: [] },
|
||||
dairy: { name: "Dairy", icon: "🧀", items: [] },
|
||||
vegetables: { name: "Vegetables", icon: "🥬", items: [] },
|
||||
fruits: { name: "Fruits", icon: "🍎", items: [] },
|
||||
grains: { name: "Grains & Legumes", icon: "🌾", items: [] },
|
||||
fats_nuts: { name: "Fats & Nuts", icon: "🥜", items: [] },
|
||||
spices: { name: "Spices & Condiments", icon: "🧂", items: [] },
|
||||
beverages: { name: "Beverages", icon: "🥤", items: [] },
|
||||
other: { name: "Other", icon: "📦", items: [] },
|
||||
};
|
||||
|
||||
for (const data of Object.values(ingredientMap)) {
|
||||
const cat = categories[data.category] || categories.other;
|
||||
cat.items.push({
|
||||
item: data.item,
|
||||
total_grams: Math.round(data.total_grams),
|
||||
display_amount: formatAmount(data.total_grams),
|
||||
total_amount: Math.round(data.total_amount),
|
||||
unit: data.unit,
|
||||
display_amount: formatAmount(data.total_amount, data.unit),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort items within each category alphabetically
|
||||
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 = {};
|
||||
for (const [key, cat] of Object.entries(categories)) {
|
||||
if (cat.items.length > 0) {
|
||||
@@ -145,18 +142,17 @@ function generateShoppingList(mealPlan) {
|
||||
}
|
||||
|
||||
return {
|
||||
title: "Haftalık Alışveriş Listesi",
|
||||
note: "Bu liste 7 günlük beslenme programınıza göre hazırlanmıştır.",
|
||||
title: "Weekly Shopping List",
|
||||
note: "This list is based on your 7-day meal plan.",
|
||||
categories: result,
|
||||
total_items: Object.values(ingredientMap).length,
|
||||
};
|
||||
}
|
||||
|
||||
function formatAmount(grams) {
|
||||
if (grams >= 1000) {
|
||||
return `${(grams / 1000).toFixed(1)} kg`;
|
||||
}
|
||||
return `${grams}g`;
|
||||
function formatAmount(amount, unit) {
|
||||
if (unit === "g" && amount >= 1000) return `${(amount / 1000).toFixed(1)} kg`;
|
||||
if (unit === "ml" && amount >= 1000) return `${(amount / 1000).toFixed(1)} L`;
|
||||
return `${Math.round(amount)} ${unit}`;
|
||||
}
|
||||
|
||||
module.exports = { generateShoppingList };
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user