From 4d6ff6dd55359a7af9c84fe5041cbee745a6011f Mon Sep 17 00:00:00 2001 From: Shane Jaroch Date: Wed, 21 Jan 2026 13:56:03 -0500 Subject: [PATCH] add recipes; format --- .clang-format | 7 + include/db/databasemanager.h | 26 +- include/db/foodrepository.h | 72 ++--- include/db/mealrepository.h | 37 +++ include/mainwindow.h | 47 +-- include/utils/string_utils.h | 8 +- include/widgets/detailswidget.h | 27 +- include/widgets/mealwidget.h | 38 +-- include/widgets/rdasettingswidget.h | 20 +- include/widgets/searchwidget.h | 29 +- include/widgets/weightinputdialog.h | 24 +- src/db/databasemanager.cpp | 223 +++++++++++---- src/db/foodrepository.cpp | 424 ++++++++++++++-------------- src/db/mealrepository.cpp | 152 ++++++++++ src/main.cpp | 119 ++++---- src/mainwindow.cpp | 286 +++++++++---------- src/utils/string_utils.cpp | 172 ++++++----- src/widgets/detailswidget.cpp | 94 +++--- src/widgets/mealwidget.cpp | 169 +++++------ src/widgets/rdasettingswidget.cpp | 103 ++++--- src/widgets/searchwidget.cpp | 188 ++++++------ src/widgets/weightinputdialog.cpp | 101 ++++--- tests/test_foodrepository.cpp | 87 +++--- 23 files changed, 1371 insertions(+), 1082 deletions(-) create mode 100644 .clang-format create mode 100644 include/db/mealrepository.h create mode 100644 src/db/mealrepository.cpp diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3f1dc11 --- /dev/null +++ b/.clang-format @@ -0,0 +1,7 @@ +BasedOnStyle: Google +IndentWidth: 4 +TabWidth: 4 +AccessModifierOffset: -4 +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: Empty +KeepEmptyLinesAtTheStartOfBlocks: false diff --git a/include/db/databasemanager.h b/include/db/databasemanager.h index f6597e6..17336fc 100644 --- a/include/db/databasemanager.h +++ b/include/db/databasemanager.h @@ -7,23 +7,23 @@ class DatabaseManager { public: - static DatabaseManager &instance(); - bool connect(const QString &path); - [[nodiscard]] bool isOpen() const; - [[nodiscard]] QSqlDatabase database() const; - [[nodiscard]] QSqlDatabase userDatabase() const; + static DatabaseManager& instance(); + bool connect(const QString& path); + [[nodiscard]] bool isOpen() const; + [[nodiscard]] QSqlDatabase database() const; + [[nodiscard]] QSqlDatabase userDatabase() const; - DatabaseManager(const DatabaseManager &) = delete; - DatabaseManager &operator=(const DatabaseManager &) = delete; + DatabaseManager(const DatabaseManager&) = delete; + DatabaseManager& operator=(const DatabaseManager&) = delete; private: - DatabaseManager(); - ~DatabaseManager(); + DatabaseManager(); + ~DatabaseManager(); - void initUserDatabase(); + void initUserDatabase(); - QSqlDatabase m_db; - QSqlDatabase m_userDb; + QSqlDatabase m_db; + QSqlDatabase m_userDb; }; -#endif // DATABASEMANAGER_H +#endif // DATABASEMANAGER_H diff --git a/include/db/foodrepository.h b/include/db/foodrepository.h index e2503e4..cb2d266 100644 --- a/include/db/foodrepository.h +++ b/include/db/foodrepository.h @@ -6,59 +6,59 @@ #include struct Nutrient { - int id; - QString description; - double amount; - QString unit; - double rdaPercentage; // Calculated + int id; + QString description; + double amount; + QString unit; + double rdaPercentage; // Calculated }; struct ServingWeight { - QString description; - double grams; + QString description; + double grams; }; struct FoodItem { - int id; - QString description; - QString foodGroupName; - int nutrientCount; - int aminoCount; - int flavCount; - int score; // For search results - std::vector nutrients; // Full details for results + int id; + QString description; + QString foodGroupName; + int nutrientCount; + int aminoCount; + int flavCount; + int score; // For search results + std::vector nutrients; // Full details for results }; class FoodRepository { public: - explicit FoodRepository(); + explicit FoodRepository(); - // Search foods by keyword - std::vector searchFoods(const QString &query); + // Search foods by keyword + std::vector searchFoods(const QString& query); - // Get detailed nutrients for a generic food (100g) - // Returns a list of nutrients - std::vector getFoodNutrients(int foodId); + // Get detailed nutrients for a generic food (100g) + // Returns a list of nutrients + std::vector getFoodNutrients(int foodId); - // Get available serving weights (units) for a food - std::vector getFoodServings(int foodId); + // Get available serving weights (units) for a food + std::vector getFoodServings(int foodId); - // RDA methods - std::map getNutrientRdas(); - void updateRda(int nutrId, double value); + // RDA methods + std::map getNutrientRdas(); + void updateRda(int nutrId, double value); - // Helper to get nutrient definition basics if needed - // QString getNutrientName(int nutrientId); + // Helper to get nutrient definition basics if needed + // QString getNutrientName(int nutrientId); private: - // Internal helper methods - void ensureCacheLoaded(); - void loadRdas(); + // Internal helper methods + void ensureCacheLoaded(); + void loadRdas(); - bool m_cacheLoaded = false; - // Cache stores basic food info - std::vector m_cache; - std::map m_rdas; + bool m_cacheLoaded = false; + // Cache stores basic food info + std::vector m_cache; + std::map m_rdas; }; -#endif // FOODREPOSITORY_H +#endif // FOODREPOSITORY_H diff --git a/include/db/mealrepository.h b/include/db/mealrepository.h new file mode 100644 index 0000000..00ca2bf --- /dev/null +++ b/include/db/mealrepository.h @@ -0,0 +1,37 @@ +#ifndef MEALREPOSITORY_H +#define MEALREPOSITORY_H + +#include +#include +#include +#include + +struct MealLogItem { + int id; // log_food.id + int foodId; + double grams; + int mealId; + QString mealName; + // Potentially cached description? + QString foodName; // Joined from food_des if we query it +}; + +class MealRepository { +public: + MealRepository(); + + // Meal Names (Breakfast, Lunch, etc.) + std::map getMealNames(); + + // Logging + void addFoodLog(int foodId, double grams, int mealId, QDate date = QDate::currentDate()); + std::vector getDailyLogs(QDate date = QDate::currentDate()); + void clearDailyLogs(QDate date = QDate::currentDate()); + void removeLogEntry(int logId); + +private: + std::map m_mealNamesCache; + void ensureMealNamesLoaded(); +}; + +#endif // MEALREPOSITORY_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 1a8c277..07a1caa 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -1,39 +1,40 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H +#include +#include + #include "widgets/detailswidget.h" #include "widgets/mealwidget.h" #include "widgets/searchwidget.h" -#include -#include class MainWindow : public QMainWindow { - Q_OBJECT + Q_OBJECT public: - MainWindow(QWidget *parent = nullptr); - ~MainWindow() override; + MainWindow(QWidget* parent = nullptr); + ~MainWindow() override; private slots: - void onOpenDatabase(); - void onRecentFileClick(); - void onSettings(); - void onAbout(); + void onOpenDatabase(); + void onRecentFileClick(); + void onSettings(); + void onAbout(); private: - void setupUi(); - void updateRecentFileActions(); - void addToRecentFiles(const QString &path); - - QTabWidget *tabs; - SearchWidget *searchWidget; - DetailsWidget *detailsWidget; - MealWidget *mealWidget; - FoodRepository repository; - - QMenu *recentFilesMenu; - static constexpr int MaxRecentFiles = 5; - QAction *recentFileActions[MaxRecentFiles]; + void setupUi(); + void updateRecentFileActions(); + void addToRecentFiles(const QString& path); + + QTabWidget* tabs; + SearchWidget* searchWidget; + DetailsWidget* detailsWidget; + MealWidget* mealWidget; + FoodRepository repository; + + QMenu* recentFilesMenu; + static constexpr int MaxRecentFiles = 5; + QAction* recentFileActions[MaxRecentFiles]; }; -#endif // MAINWINDOW_H +#endif // MAINWINDOW_H diff --git a/include/utils/string_utils.h b/include/utils/string_utils.h index 58c07b1..1c52fdd 100644 --- a/include/utils/string_utils.h +++ b/include/utils/string_utils.h @@ -8,12 +8,12 @@ namespace Utils { // Calculate Levenshtein distance between two strings -int levenshteinDistance(const QString &s1, const QString &s2); +int levenshteinDistance(const QString& s1, const QString& s2); // Calculate a simple fuzzy match score (0-100) // Higher is better. -int calculateFuzzyScore(const QString &query, const QString &target); +int calculateFuzzyScore(const QString& query, const QString& target); -} // namespace Utils +} // namespace Utils -#endif // STRING_UTILS_H +#endif // STRING_UTILS_H diff --git a/include/widgets/detailswidget.h b/include/widgets/detailswidget.h index 51b74e6..8bb23d5 100644 --- a/include/widgets/detailswidget.h +++ b/include/widgets/detailswidget.h @@ -1,34 +1,35 @@ #ifndef DETAILSWIDGET_H #define DETAILSWIDGET_H -#include "db/foodrepository.h" #include #include #include #include +#include "db/foodrepository.h" + class DetailsWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit DetailsWidget(QWidget *parent = nullptr); + explicit DetailsWidget(QWidget* parent = nullptr); - void loadFood(int foodId, const QString &foodName); + void loadFood(int foodId, const QString& foodName); signals: - void addToMeal(int foodId, const QString &foodName, double grams); + void addToMeal(int foodId, const QString& foodName, double grams); private slots: - void onAddClicked(); + void onAddClicked(); private: - QLabel *nameLabel; - QTableWidget *nutrientsTable; - QPushButton *addButton; - FoodRepository repository; + QLabel* nameLabel; + QTableWidget* nutrientsTable; + QPushButton* addButton; + FoodRepository repository; - int currentFoodId; - QString currentFoodName; + int currentFoodId; + QString currentFoodName; }; -#endif // DETAILSWIDGET_H +#endif // DETAILSWIDGET_H diff --git a/include/widgets/mealwidget.h b/include/widgets/mealwidget.h index cbf163b..041a4ba 100644 --- a/include/widgets/mealwidget.h +++ b/include/widgets/mealwidget.h @@ -1,40 +1,44 @@ #ifndef MEALWIDGET_H #define MEALWIDGET_H -#include "db/foodrepository.h" #include #include #include -#include #include +#include "db/foodrepository.h" +#include "db/mealrepository.h" + struct MealItem { - int foodId; - QString name; - double grams; - std::vector nutrients_100g; // Base nutrients + int foodId; + QString name; + double grams; + std::vector nutrients_100g; }; class MealWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit MealWidget(QWidget *parent = nullptr); + explicit MealWidget(QWidget* parent = nullptr); - void addFood(int foodId, const QString &foodName, double grams); + void addFood(int foodId, const QString& foodName, double grams); private slots: - void clearMeal(); + void clearMeal(); private: - void updateTotals(); + void updateTotals(); + void refresh(); + + QTableWidget* itemsTable; + QPushButton* clearButton; + QTableWidget* totalsTable; - QTableWidget *itemsTable; - QTableWidget *totalsTable; - QPushButton *clearButton; + FoodRepository repository; + MealRepository m_mealRepo; - std::vector mealItems; - FoodRepository repository; + std::vector mealItems; }; -#endif // MEALWIDGET_H +#endif // MEALWIDGET_H diff --git a/include/widgets/rdasettingswidget.h b/include/widgets/rdasettingswidget.h index c8284f0..54c17cc 100644 --- a/include/widgets/rdasettingswidget.h +++ b/include/widgets/rdasettingswidget.h @@ -1,26 +1,26 @@ #ifndef RDASETTINGSWIDGET_H #define RDASETTINGSWIDGET_H -#include "db/foodrepository.h" #include #include +#include "db/foodrepository.h" + class RDASettingsWidget : public QDialog { - Q_OBJECT + Q_OBJECT public: - explicit RDASettingsWidget(FoodRepository &repository, - QWidget *parent = nullptr); + explicit RDASettingsWidget(FoodRepository& repository, QWidget* parent = nullptr); private slots: - void onCellChanged(int row, int column); + void onCellChanged(int row, int column); private: - void loadData(); + void loadData(); - FoodRepository &m_repository; - QTableWidget *m_table; - bool m_loading = false; + FoodRepository& m_repository; + QTableWidget* m_table; + bool m_loading = false; }; -#endif // RDASETTINGSWIDGET_H +#endif // RDASETTINGSWIDGET_H diff --git a/include/widgets/searchwidget.h b/include/widgets/searchwidget.h index 03d929e..9b109aa 100644 --- a/include/widgets/searchwidget.h +++ b/include/widgets/searchwidget.h @@ -1,34 +1,35 @@ #ifndef SEARCHWIDGET_H #define SEARCHWIDGET_H -#include "db/foodrepository.h" #include #include #include #include #include +#include "db/foodrepository.h" + class SearchWidget : public QWidget { - Q_OBJECT + Q_OBJECT public: - explicit SearchWidget(QWidget *parent = nullptr); + explicit SearchWidget(QWidget* parent = nullptr); signals: - void foodSelected(int foodId, const QString &foodName); - void addToMealRequested(int foodId, const QString &foodName, double grams); + void foodSelected(int foodId, const QString& foodName); + void addToMealRequested(int foodId, const QString& foodName, double grams); private slots: - void performSearch(); - void onRowDoubleClicked(int row, int column); - void onCustomContextMenu(const QPoint &pos); + void performSearch(); + void onRowDoubleClicked(int row, int column); + void onCustomContextMenu(const QPoint& pos); private: - QLineEdit *searchInput; - QPushButton *searchButton; - QTableWidget *resultsTable; - FoodRepository repository; - QTimer *searchTimer; + QLineEdit* searchInput; + QPushButton* searchButton; + QTableWidget* resultsTable; + FoodRepository repository; + QTimer* searchTimer; }; -#endif // SEARCHWIDGET_H +#endif // SEARCHWIDGET_H diff --git a/include/widgets/weightinputdialog.h b/include/widgets/weightinputdialog.h index bbc13a4..e978172 100644 --- a/include/widgets/weightinputdialog.h +++ b/include/widgets/weightinputdialog.h @@ -1,29 +1,29 @@ #ifndef WEIGHTINPUTDIALOG_H #define WEIGHTINPUTDIALOG_H -#include "db/foodrepository.h" #include #include #include #include +#include "db/foodrepository.h" + class WeightInputDialog : public QDialog { - Q_OBJECT + Q_OBJECT public: - explicit WeightInputDialog(const QString &foodName, - const std::vector &servings, - QWidget *parent = nullptr); + explicit WeightInputDialog(const QString& foodName, const std::vector& servings, + QWidget* parent = nullptr); - double getGrams() const; + double getGrams() const; private: - QDoubleSpinBox *amountSpinBox; - QComboBox *unitComboBox; - std::vector m_servings; + QDoubleSpinBox* amountSpinBox; + QComboBox* unitComboBox; + std::vector m_servings; - static constexpr double GRAMS_PER_OZ = 28.3495; - static constexpr double GRAMS_PER_LB = 453.592; + static constexpr double GRAMS_PER_OZ = 28.3495; + static constexpr double GRAMS_PER_LB = 453.592; }; -#endif // WEIGHTINPUTDIALOG_H +#endif // WEIGHTINPUTDIALOG_H diff --git a/src/db/databasemanager.cpp b/src/db/databasemanager.cpp index 131b870..e64b75e 100644 --- a/src/db/databasemanager.cpp +++ b/src/db/databasemanager.cpp @@ -1,83 +1,192 @@ #include "db/databasemanager.h" + #include #include #include #include #include -DatabaseManager &DatabaseManager::instance() { - static DatabaseManager instance; - return instance; +DatabaseManager& DatabaseManager::instance() { + static DatabaseManager instance; + return instance; } DatabaseManager::DatabaseManager() { - m_userDb = QSqlDatabase::addDatabase("QSQLITE", "user_db"); - initUserDatabase(); + m_userDb = QSqlDatabase::addDatabase("QSQLITE", "user_db"); + initUserDatabase(); } DatabaseManager::~DatabaseManager() { - if (m_db.isOpen()) { - m_db.close(); - } + if (m_db.isOpen()) { + m_db.close(); + } } -bool DatabaseManager::connect(const QString &path) { - if (m_db.isOpen()) { - return true; - } +bool DatabaseManager::connect(const QString& path) { + if (m_db.isOpen()) { + return true; + } - if (!QFileInfo::exists(path)) { - qCritical() << "Database file not found:" << path; - return false; - } + if (!QFileInfo::exists(path)) { + qCritical() << "Database file not found:" << path; + return false; + } - m_db = QSqlDatabase::addDatabase("QSQLITE"); - m_db.setDatabaseName(path); - m_db.setConnectOptions("QSQLITE_OPEN_READONLY"); + m_db = QSqlDatabase::addDatabase("QSQLITE"); + m_db.setDatabaseName(path); + m_db.setConnectOptions("QSQLITE_OPEN_READONLY"); - if (!m_db.open()) { - qCritical() << "Error opening database:" << m_db.lastError().text(); - return false; - } + if (!m_db.open()) { + qCritical() << "Error opening database:" << m_db.lastError().text(); + return false; + } - return true; + return true; } -bool DatabaseManager::isOpen() const { return m_db.isOpen(); } +bool DatabaseManager::isOpen() const { + return m_db.isOpen(); +} -QSqlDatabase DatabaseManager::database() const { return m_db; } +QSqlDatabase DatabaseManager::database() const { + return m_db; +} -QSqlDatabase DatabaseManager::userDatabase() const { return m_userDb; } +QSqlDatabase DatabaseManager::userDatabase() const { + return m_userDb; +} void DatabaseManager::initUserDatabase() { - QString path = QDir::homePath() + "/.nutra/nt.sqlite3"; - m_userDb.setDatabaseName(path); - - if (!m_userDb.open()) { - qCritical() << "Failed to open user database:" - << m_userDb.lastError().text(); - return; - } - - QSqlQuery query(m_userDb); - // Create profile table (simplified version of CLI's schema) - if (!query.exec("CREATE TABLE IF NOT EXISTS profile (" - "id INTEGER PRIMARY KEY AUTOINCREMENT, " - "name TEXT UNIQUE NOT NULL)")) { - qCritical() << "Failed to create profile table:" - << query.lastError().text(); - } - - // Ensure default profile exists - query.exec("INSERT OR IGNORE INTO profile (id, name) VALUES (1, 'default')"); - - // Create rda table - if (!query.exec("CREATE TABLE IF NOT EXISTS rda (" - "profile_id INTEGER NOT NULL, " - "nutr_id INTEGER NOT NULL, " - "rda REAL NOT NULL, " - "PRIMARY KEY (profile_id, nutr_id), " - "FOREIGN KEY (profile_id) REFERENCES profile (id))")) { - qCritical() << "Failed to create rda table:" << query.lastError().text(); - } + QString path = QDir::homePath() + "/.nutra/nt.sqlite3"; + m_userDb.setDatabaseName(path); + + if (!m_userDb.open()) { + qCritical() << "Failed to open user database:" << m_userDb.lastError().text(); + return; + } + + QSqlQuery query(m_userDb); + + // Helper to execute schema creation + auto createTable = [&](const QString& sql) { + if (!query.exec(sql)) { + qCritical() << "Failed to create table:" << query.lastError().text() << "\nSQL:" << sql; + } + }; + + createTable( + "CREATE TABLE IF NOT EXISTS version (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "version text NOT NULL UNIQUE, " + "created date NOT NULL, " + "notes text)"); + + createTable( + "CREATE TABLE IF NOT EXISTS bmr_eq (" + "id integer PRIMARY KEY, " + "name text NOT NULL UNIQUE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS bf_eq (" + "id integer PRIMARY KEY, " + "name text NOT NULL UNIQUE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS profile (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "uuid int NOT NULL DEFAULT (RANDOM()), " + "name text NOT NULL UNIQUE, " + "gender text, " + "dob date, " + "act_lvl int DEFAULT 2, " + "goal_wt real, " + "goal_bf real DEFAULT 18, " + "bmr_eq_id int DEFAULT 1, " + "bf_eq_id int DEFAULT 1, " + "created int DEFAULT (strftime ('%s', 'now')), " + "FOREIGN KEY (bmr_eq_id) REFERENCES bmr_eq (id) ON UPDATE " + "CASCADE ON DELETE CASCADE, " + "FOREIGN KEY (bf_eq_id) REFERENCES bf_eq (id) ON UPDATE CASCADE " + "ON DELETE CASCADE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS rda (" + "profile_id int NOT NULL, " + "nutr_id int NOT NULL, " + "rda real NOT NULL, " + "PRIMARY KEY (profile_id, nutr_id), " + "FOREIGN KEY (profile_id) REFERENCES profile (id) ON UPDATE " + "CASCADE ON DELETE CASCADE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS custom_food (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "tagname text NOT NULL UNIQUE, " + "name text NOT NULL UNIQUE, " + "created int DEFAULT (strftime ('%s', 'now')))"); + + createTable( + "CREATE TABLE IF NOT EXISTS cf_dat (" + "cf_id int NOT NULL, " + "nutr_id int NOT NULL, " + "nutr_val real NOT NULL, " + "notes text, " + "created int DEFAULT (strftime ('%s', 'now')), " + "PRIMARY KEY (cf_id, nutr_id), " + "FOREIGN KEY (cf_id) REFERENCES custom_food (id) ON UPDATE " + "CASCADE ON DELETE CASCADE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS meal_name (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "name text NOT NULL)"); + + createTable( + "CREATE TABLE IF NOT EXISTS log_food (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "profile_id int NOT NULL, " + "date int DEFAULT (strftime ('%s', 'now')), " + "meal_id int NOT NULL, " + "food_id int NOT NULL, " + "msre_id int NOT NULL, " + "amt real NOT NULL, " + "created int DEFAULT (strftime ('%s', 'now')), " + "FOREIGN KEY (profile_id) REFERENCES profile (id) ON UPDATE " + "CASCADE ON DELETE CASCADE, " + "FOREIGN KEY (meal_id) REFERENCES meal_name (id) ON UPDATE " + "CASCADE ON DELETE CASCADE)"); + + createTable( + "CREATE TABLE IF NOT EXISTS log_cf (" + "id integer PRIMARY KEY AUTOINCREMENT, " + "profile_id int NOT NULL, " + "date int DEFAULT (strftime ('%s', 'now')), " + "meal_id int NOT NULL, " + "food_id int NOT NULL, " + "custom_food_id int, " + "msre_id int NOT NULL, " + "amt real NOT NULL, " + "created int DEFAULT (strftime ('%s', 'now')), " + "FOREIGN KEY (profile_id) REFERENCES profile (id) ON UPDATE " + "CASCADE ON DELETE CASCADE, " + "FOREIGN KEY (meal_id) REFERENCES meal_name (id) ON UPDATE " + "CASCADE ON DELETE CASCADE, " + "FOREIGN KEY (custom_food_id) REFERENCES custom_food (id) ON " + "UPDATE CASCADE ON DELETE CASCADE)"); + + // Default Data Seeding + + // Ensure default profile exists + query.exec("INSERT OR IGNORE INTO profile (id, name) VALUES (1, 'default')"); + + // Seed standard meal names if table is empty + query.exec("SELECT count(*) FROM meal_name"); + if (query.next() && query.value(0).toInt() == 0) { + QStringList meals = {"Breakfast", "Lunch", "Dinner", "Snack", "Brunch"}; + for (const auto& meal : meals) { + query.prepare("INSERT INTO meal_name (name) VALUES (?)"); + query.addBindValue(meal); + query.exec(); + } + } } diff --git a/src/db/foodrepository.cpp b/src/db/foodrepository.cpp index a186895..c878723 100644 --- a/src/db/foodrepository.cpp +++ b/src/db/foodrepository.cpp @@ -1,269 +1,261 @@ #include "db/foodrepository.h" -#include "db/databasemanager.h" + #include #include #include #include #include +#include "db/databasemanager.h" + FoodRepository::FoodRepository() {} -#include "utils/string_utils.h" #include +#include "utils/string_utils.h" + // ... void FoodRepository::ensureCacheLoaded() { - if (m_cacheLoaded) - return; - - QSqlDatabase db = DatabaseManager::instance().database(); - if (!db.isOpen()) - return; - - // 1. Load Food Items with Group Names - QSqlQuery query("SELECT f.id, f.long_desc, g.fdgrp_desc " - "FROM food_des f " - "JOIN fdgrp g ON f.fdgrp_id = g.id", - db); - std::map nutrientCounts; - - // 2. Load Nutrient Counts (Bulk) - QSqlQuery countQuery( - "SELECT food_id, count(*) FROM nut_data GROUP BY food_id", db); - while (countQuery.next()) { - nutrientCounts[countQuery.value(0).toInt()] = countQuery.value(1).toInt(); - } - - while (query.next()) { - FoodItem item; - item.id = query.value(0).toInt(); - item.description = query.value(1).toString(); - item.foodGroupName = query.value(2).toString(); - - // Set counts from map (default 0 if not found) - auto it = nutrientCounts.find(item.id); - item.nutrientCount = (it != nutrientCounts.end()) ? it->second : 0; - - item.aminoCount = 0; // TODO: Implement specific counts if needed - item.flavCount = 0; - item.score = 0; - m_cache.push_back(item); - } - loadRdas(); - m_cacheLoaded = true; -} - -void FoodRepository::loadRdas() { - m_rdas.clear(); - QSqlDatabase db = DatabaseManager::instance().database(); - QSqlDatabase userDb = DatabaseManager::instance().userDatabase(); + if (m_cacheLoaded) return; - // 1. Load Defaults from USDA - if (db.isOpen()) { - QSqlQuery query("SELECT id, rda FROM nutrients_overview", db); - while (query.next()) { - m_rdas[query.value(0).toInt()] = query.value(1).toDouble(); + QSqlDatabase db = DatabaseManager::instance().database(); + if (!db.isOpen()) return; + + // 1. Load Food Items with Group Names + QSqlQuery query( + "SELECT f.id, f.long_desc, g.fdgrp_desc " + "FROM food_des f " + "JOIN fdgrp g ON f.fdgrp_id = g.id", + db); + std::map nutrientCounts; + + // 2. Load Nutrient Counts (Bulk) + QSqlQuery countQuery("SELECT food_id, count(*) FROM nut_data GROUP BY food_id", db); + while (countQuery.next()) { + nutrientCounts[countQuery.value(0).toInt()] = countQuery.value(1).toInt(); } - } - // 2. Load Overrides from User DB - if (userDb.isOpen()) { - QSqlQuery query("SELECT nutr_id, rda FROM rda WHERE profile_id = 1", - userDb); while (query.next()) { - m_rdas[query.value(0).toInt()] = query.value(1).toDouble(); + FoodItem item; + item.id = query.value(0).toInt(); + item.description = query.value(1).toString(); + item.foodGroupName = query.value(2).toString(); + + // Set counts from map (default 0 if not found) + auto it = nutrientCounts.find(item.id); + item.nutrientCount = (it != nutrientCounts.end()) ? it->second : 0; + + item.aminoCount = 0; // TODO: Implement specific counts if needed + item.flavCount = 0; + item.score = 0; + m_cache.push_back(item); } - } + loadRdas(); + m_cacheLoaded = true; } -std::vector FoodRepository::searchFoods(const QString &query) { - ensureCacheLoaded(); - std::vector results; +void FoodRepository::loadRdas() { + m_rdas.clear(); + QSqlDatabase db = DatabaseManager::instance().database(); + QSqlDatabase userDb = DatabaseManager::instance().userDatabase(); + + // 1. Load Defaults from USDA + if (db.isOpen()) { + QSqlQuery query("SELECT id, rda FROM nutrients_overview", db); + while (query.next()) { + m_rdas[query.value(0).toInt()] = query.value(1).toDouble(); + } + } - if (query.trimmed().isEmpty()) - return results; + // 2. Load Overrides from User DB + if (userDb.isOpen()) { + QSqlQuery query("SELECT nutr_id, rda FROM rda WHERE profile_id = 1", userDb); + while (query.next()) { + m_rdas[query.value(0).toInt()] = query.value(1).toDouble(); + } + } +} - // Calculate scores - // create a temporary list of pointers or indices to sort? - // Copying might be expensive if cache is huge (8k items is fine though) - - // Let's iterate and keep top matches. - struct ScoredItem { - const FoodItem *item; - int score; - }; - std::vector scoredItems; - scoredItems.reserve(m_cache.size()); - - for (const auto &item : m_cache) { - int score = Utils::calculateFuzzyScore(query, item.description); - if (score > 40) { // Threshold - scoredItems.push_back({&item, score}); +std::vector FoodRepository::searchFoods(const QString& query) { + ensureCacheLoaded(); + std::vector results; + + if (query.trimmed().isEmpty()) return results; + + // Calculate scores + // create a temporary list of pointers or indices to sort? + // Copying might be expensive if cache is huge (8k items is fine though) + + // Let's iterate and keep top matches. + struct ScoredItem { + const FoodItem* item; + int score; + }; + std::vector scoredItems; + scoredItems.reserve(m_cache.size()); + + for (const auto& item : m_cache) { + int score = Utils::calculateFuzzyScore(query, item.description); + if (score > 40) { // Threshold + scoredItems.push_back({&item, score}); + } } - } - - // Sort by score desc - std::sort(scoredItems.begin(), scoredItems.end(), - [](const ScoredItem &a, const ScoredItem &b) { - return a.score > b.score; - }); - - // Take top 100 - int count = 0; - std::vector resultIds; - std::map idToIndex; - - for (const auto &si : scoredItems) { - if (count >= 100) - break; - FoodItem res = *si.item; - res.score = si.score; - // We will populate nutrients shortly - results.push_back(res); - resultIds.push_back(res.id); - idToIndex[res.id] = count; - count++; - } - - // Batch fetch nutrients for these results - if (!resultIds.empty()) { - QSqlDatabase db = DatabaseManager::instance().database(); - QStringList idStrings; - for (int id : resultIds) - idStrings << QString::number(id); - - QString sql = - QString("SELECT n.food_id, n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " - "FROM nut_data n " - "JOIN nutr_def d ON n.nutr_id = d.id " - "WHERE n.food_id IN (%1)") - .arg(idStrings.join(",")); - - QSqlQuery nutQuery(sql, db); - while (nutQuery.next()) { - int fid = nutQuery.value(0).toInt(); - Nutrient nut; - nut.id = nutQuery.value(1).toInt(); - nut.amount = nutQuery.value(2).toDouble(); - nut.description = nutQuery.value(3).toString(); - nut.unit = nutQuery.value(4).toString(); - - if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) { - nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0; - } else { - nut.rdaPercentage = 0.0; - } - - if (idToIndex.count(fid) != 0U) { - results[idToIndex[fid]].nutrients.push_back(nut); - } + + // Sort by score desc + std::sort(scoredItems.begin(), scoredItems.end(), + [](const ScoredItem& a, const ScoredItem& b) { return a.score > b.score; }); + + // Take top 100 + int count = 0; + std::vector resultIds; + std::map idToIndex; + + for (const auto& si : scoredItems) { + if (count >= 100) break; + FoodItem res = *si.item; + res.score = si.score; + // We will populate nutrients shortly + results.push_back(res); + resultIds.push_back(res.id); + idToIndex[res.id] = count; + count++; } - // Update counts based on actual data - for (auto &res : results) { - res.nutrientCount = static_cast(res.nutrients.size()); - // TODO: Logic for amino/flav counts if we have ranges of IDs + // Batch fetch nutrients for these results + if (!resultIds.empty()) { + QSqlDatabase db = DatabaseManager::instance().database(); + QStringList idStrings; + for (int id : resultIds) idStrings << QString::number(id); + + QString sql = QString( + "SELECT n.food_id, n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " + "FROM nut_data n " + "JOIN nutr_def d ON n.nutr_id = d.id " + "WHERE n.food_id IN (%1)") + .arg(idStrings.join(",")); + + QSqlQuery nutQuery(sql, db); + while (nutQuery.next()) { + int fid = nutQuery.value(0).toInt(); + Nutrient nut; + nut.id = nutQuery.value(1).toInt(); + nut.amount = nutQuery.value(2).toDouble(); + nut.description = nutQuery.value(3).toString(); + nut.unit = nutQuery.value(4).toString(); + + if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) { + nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0; + } else { + nut.rdaPercentage = 0.0; + } + + if (idToIndex.count(fid) != 0U) { + results[idToIndex[fid]].nutrients.push_back(nut); + } + } + + // Update counts based on actual data + for (auto& res : results) { + res.nutrientCount = static_cast(res.nutrients.size()); + // TODO: Logic for amino/flav counts if we have ranges of IDs + } } - } - return results; + return results; } std::vector FoodRepository::getFoodNutrients(int foodId) { - std::vector results; - QSqlDatabase db = DatabaseManager::instance().database(); + std::vector results; + QSqlDatabase db = DatabaseManager::instance().database(); - if (!db.isOpen()) - return results; + if (!db.isOpen()) return results; + + QSqlQuery query(db); + if (!query.prepare("SELECT n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " + "FROM nut_data n " + "JOIN nutr_def d ON n.nutr_id = d.id " + "WHERE n.food_id = ?")) { + qCritical() << "Prepare failed:" << query.lastError().text(); + return results; + } - QSqlQuery query(db); - if (!query.prepare("SELECT n.nutr_id, n.nutr_val, d.nutr_desc, d.unit " - "FROM nut_data n " - "JOIN nutr_def d ON n.nutr_id = d.id " - "WHERE n.food_id = ?")) { + query.bindValue(0, foodId); - qCritical() << "Prepare failed:" << query.lastError().text(); - return results; - } + if (query.exec()) { + while (query.next()) { + Nutrient nut; + nut.id = query.value(0).toInt(); + nut.amount = query.value(1).toDouble(); + nut.description = query.value(2).toString(); + nut.unit = query.value(3).toString(); - query.bindValue(0, foodId); + if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) { + nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0; + } else { + nut.rdaPercentage = 0.0; + } - if (query.exec()) { - while (query.next()) { - Nutrient nut; - nut.id = query.value(0).toInt(); - nut.amount = query.value(1).toDouble(); - nut.description = query.value(2).toString(); - nut.unit = query.value(3).toString(); - - if (m_rdas.count(nut.id) != 0U && m_rdas[nut.id] > 0) { - nut.rdaPercentage = (nut.amount / m_rdas[nut.id]) * 100.0; - } else { - nut.rdaPercentage = 0.0; - } - - results.push_back(nut); - } + results.push_back(nut); + } - } else { - qCritical() << "Nutrient query failed:" << query.lastError().text(); - } + } else { + qCritical() << "Nutrient query failed:" << query.lastError().text(); + } - return results; + return results; } std::vector FoodRepository::getFoodServings(int foodId) { - std::vector results; - QSqlDatabase db = DatabaseManager::instance().database(); - - if (!db.isOpen()) - return results; + std::vector results; + QSqlDatabase db = DatabaseManager::instance().database(); - QSqlQuery query(db); - if (!query.prepare("SELECT d.msre_desc, s.grams " - "FROM serving s " - "JOIN serv_desc d ON s.msre_id = d.id " - "WHERE s.food_id = ?")) { - qCritical() << "Prepare servings failed:" << query.lastError().text(); - return results; - } + if (!db.isOpen()) return results; - query.bindValue(0, foodId); + QSqlQuery query(db); + if (!query.prepare("SELECT d.msre_desc, s.grams " + "FROM serving s " + "JOIN serv_desc d ON s.msre_id = d.id " + "WHERE s.food_id = ?")) { + qCritical() << "Prepare servings failed:" << query.lastError().text(); + return results; + } - if (query.exec()) { - while (query.next()) { - ServingWeight sw; - sw.description = query.value(0).toString(); - sw.grams = query.value(1).toDouble(); - results.push_back(sw); + query.bindValue(0, foodId); + + if (query.exec()) { + while (query.next()) { + ServingWeight sw; + sw.description = query.value(0).toString(); + sw.grams = query.value(1).toDouble(); + results.push_back(sw); + } + } else { + qCritical() << "Servings query failed:" << query.lastError().text(); } - } else { - qCritical() << "Servings query failed:" << query.lastError().text(); - } - return results; + return results; } std::map FoodRepository::getNutrientRdas() { - ensureCacheLoaded(); - return m_rdas; + ensureCacheLoaded(); + return m_rdas; } void FoodRepository::updateRda(int nutrId, double value) { - QSqlDatabase userDb = DatabaseManager::instance().userDatabase(); - if (!userDb.isOpen()) - return; - - QSqlQuery query(userDb); - query.prepare("INSERT OR REPLACE INTO rda (profile_id, nutr_id, rda) " - "VALUES (1, ?, ?)"); - query.bindValue(0, nutrId); - query.bindValue(1, value); - - if (query.exec()) { - m_rdas[nutrId] = value; - } else { - qCritical() << "Failed to update RDA:" << query.lastError().text(); - } + QSqlDatabase userDb = DatabaseManager::instance().userDatabase(); + if (!userDb.isOpen()) return; + + QSqlQuery query(userDb); + query.prepare( + "INSERT OR REPLACE INTO rda (profile_id, nutr_id, rda) " + "VALUES (1, ?, ?)"); + query.bindValue(0, nutrId); + query.bindValue(1, value); + + if (query.exec()) { + m_rdas[nutrId] = value; + } else { + qCritical() << "Failed to update RDA:" << query.lastError().text(); + } } diff --git a/src/db/mealrepository.cpp b/src/db/mealrepository.cpp new file mode 100644 index 0000000..0fd1e14 --- /dev/null +++ b/src/db/mealrepository.cpp @@ -0,0 +1,152 @@ +#include "db/mealrepository.h" + +#include +#include +#include +#include +#include + +#include "db/databasemanager.h" + +MealRepository::MealRepository() {} + +void MealRepository::ensureMealNamesLoaded() { + if (!m_mealNamesCache.empty()) return; + + QSqlDatabase db = DatabaseManager::instance().userDatabase(); + if (!db.isOpen()) return; + + QSqlQuery query("SELECT id, name FROM meal_name", db); + while (query.next()) { + m_mealNamesCache[query.value(0).toInt()] = query.value(1).toString(); + } +} + +std::map MealRepository::getMealNames() { + ensureMealNamesLoaded(); + return m_mealNamesCache; +} + +void MealRepository::addFoodLog(int foodId, double grams, int mealId, QDate date) { + QSqlDatabase db = DatabaseManager::instance().userDatabase(); + if (!db.isOpen()) return; + + // Use current time if today, otherwise noon of target date + qint64 timestamp; + if (date == QDate::currentDate()) { + timestamp = QDateTime::currentSecsSinceEpoch(); + } else { + timestamp = date.startOfDay().toSecsSinceEpoch() + 43200; // Noon + } + + QSqlQuery query(db); + query.prepare( + "INSERT INTO log_food (profile_id, date, meal_id, food_id, msre_id, amt) " + "VALUES (1, ?, ?, ?, 0, ?)"); // msre_id 0 for default/grams + query.addBindValue(timestamp); + query.addBindValue(mealId); + query.addBindValue(foodId); + query.addBindValue(grams); + + if (!query.exec()) { + qCritical() << "Failed to add food log:" << query.lastError().text(); + } +} + +std::vector MealRepository::getDailyLogs(QDate date) { + std::vector results; + QSqlDatabase userDb = DatabaseManager::instance().userDatabase(); + QSqlDatabase mainDb = DatabaseManager::instance().database(); + + if (!userDb.isOpen()) return results; + + ensureMealNamesLoaded(); + + qint64 startOfDay = date.startOfDay().toSecsSinceEpoch(); + qint64 endOfDay = date.endOfDay().toSecsSinceEpoch(); + + QSqlQuery query(userDb); + query.prepare( + "SELECT id, food_id, meal_id, amt FROM log_food " + "WHERE date >= ? AND date <= ? AND profile_id = 1"); + query.addBindValue(startOfDay); + query.addBindValue(endOfDay); + + std::vector foodIds; + + if (query.exec()) { + while (query.next()) { + MealLogItem item; + item.id = query.value(0).toInt(); + item.foodId = query.value(1).toInt(); + item.mealId = query.value(2).toInt(); + item.grams = query.value(3).toDouble(); + + if (m_mealNamesCache.count(item.mealId)) { + item.mealName = m_mealNamesCache[item.mealId]; + } else { + item.mealName = "Unknown"; + } + + results.push_back(item); + foodIds.push_back(item.foodId); + } + } else { + qCritical() << "Failed to fetch daily logs:" << query.lastError().text(); + } + + // Hydrate food names from Main DB + if (!foodIds.empty() && mainDb.isOpen()) { + QStringList idStrings; + for (int id : foodIds) idStrings << QString::number(id); + + // Simple name fetch + // Optimization: Could use FoodRepository cache if available, but direct + // query is safe here + QString sql = + QString("SELECT id, long_desc FROM food_des WHERE id IN (%1)").arg(idStrings.join(",")); + QSqlQuery nameQuery(sql, mainDb); + + std::map names; + while (nameQuery.next()) { + names[nameQuery.value(0).toInt()] = nameQuery.value(1).toString(); + } + + for (auto& item : results) { + if (names.count(item.foodId)) { + item.foodName = names[item.foodId]; + } else { + item.foodName = "Unknown Food"; // Should not happen if DBs consistent + } + } + } + + return results; +} + +void MealRepository::clearDailyLogs(QDate date) { + QSqlDatabase db = DatabaseManager::instance().userDatabase(); + if (!db.isOpen()) return; + + qint64 startOfDay = date.startOfDay().toSecsSinceEpoch(); + qint64 endOfDay = date.endOfDay().toSecsSinceEpoch(); + + QSqlQuery query(db); + query.prepare("DELETE FROM log_food WHERE date >= ? AND date <= ? AND profile_id = 1"); + query.addBindValue(startOfDay); + query.addBindValue(endOfDay); + + if (!query.exec()) { + qCritical() << "Failed to clear daily logs:" << query.lastError().text(); + } +} + +void MealRepository::removeLogEntry(int logId) { + QSqlDatabase db = DatabaseManager::instance().userDatabase(); + if (!db.isOpen()) return; + + QSqlQuery query(db); + query.prepare("DELETE FROM log_food WHERE id = ?"); + query.addBindValue(logId); + query.exec(); +} diff --git a/src/main.cpp b/src/main.cpp index 27bfce4..126ffc8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,3 @@ -#include "db/databasemanager.h" -#include "mainwindow.h" #include #include #include @@ -9,74 +7,75 @@ #include #include -int main(int argc, char *argv[]) { - QApplication app(argc, argv); - QApplication::setApplicationName("Nutra"); - QApplication::setOrganizationName("NutraTech"); - QApplication::setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); +#include "db/databasemanager.h" +#include "mainwindow.h" - // Connect to database - // Search order: - // 1. Environment variable NUTRA_DB_PATH - // 2. Local user data: ~/.local/share/nutra/usda.sqlite3 - // 3. System install: /usr/local/share/nutra/usda.sqlite3 - // 4. System install: /usr/share/nutra/usda.sqlite3 - // 5. Legacy: ~/.nutra/usda.sqlite3 +int main(int argc, char* argv[]) { + QApplication app(argc, argv); + QApplication::setApplicationName("Nutra"); + QApplication::setOrganizationName("NutraTech"); + QApplication::setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); - QStringList searchPaths; - QString envPath = qEnvironmentVariable("NUTRA_DB_PATH"); - if (!envPath.isEmpty()) - searchPaths << envPath; + // Connect to database + // Search order: + // 1. Environment variable NUTRA_DB_PATH + // 2. Local user data: ~/.local/share/nutra/usda.sqlite3 + // 3. System install: /usr/local/share/nutra/usda.sqlite3 + // 4. System install: /usr/share/nutra/usda.sqlite3 + // 5. Legacy: ~/.nutra/usda.sqlite3 - searchPaths << QStandardPaths::locate(QStandardPaths::AppDataLocation, - "usda.sqlite3", - QStandardPaths::LocateFile); - searchPaths << QDir::homePath() + "/.local/share/nutra/usda.sqlite3"; - searchPaths << "/usr/local/share/nutra/usda.sqlite3"; - searchPaths << "/usr/share/nutra/usda.sqlite3"; - searchPaths << QDir::homePath() + "/.nutra/usda.sqlite3"; + QStringList searchPaths; + QString envPath = qEnvironmentVariable("NUTRA_DB_PATH"); + if (!envPath.isEmpty()) searchPaths << envPath; - QString dbPath; - for (const QString &path : searchPaths) { - if (!path.isEmpty() && QFileInfo::exists(path)) { - dbPath = path; - break; - } - } + searchPaths << QStandardPaths::locate(QStandardPaths::AppDataLocation, "usda.sqlite3", + QStandardPaths::LocateFile); + searchPaths << QDir::homePath() + "/.local/share/nutra/usda.sqlite3"; + searchPaths << "/usr/local/share/nutra/usda.sqlite3"; + searchPaths << "/usr/share/nutra/usda.sqlite3"; + searchPaths << QDir::homePath() + "/.nutra/usda.sqlite3"; - if (dbPath.isEmpty()) { - qWarning() << "Database not found in standard locations."; - QMessageBox::StandardButton resBtn = QMessageBox::question( - nullptr, "Database Not Found", - "The Nutrient database (usda.sqlite3) was not found.\nWould you like " - "to browse for it manually?", - QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes); - - if (resBtn == QMessageBox::Yes) { - dbPath = QFileDialog::getOpenFileName( - nullptr, "Find usda.sqlite3", QDir::homePath() + "/.nutra", - "SQLite Databases (*.sqlite3 *.db)"); + QString dbPath; + for (const QString& path : searchPaths) { + if (!path.isEmpty() && QFileInfo::exists(path)) { + dbPath = path; + break; + } } if (dbPath.isEmpty()) { - return 1; // User cancelled or still not found + qWarning() << "Database not found in standard locations."; + QMessageBox::StandardButton resBtn = QMessageBox::question( + nullptr, "Database Not Found", + "The Nutrient database (usda.sqlite3) was not found.\nWould you like " + "to browse for it manually?", + QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes); + + if (resBtn == QMessageBox::Yes) { + dbPath = QFileDialog::getOpenFileName(nullptr, "Find usda.sqlite3", + QDir::homePath() + "/.nutra", + "SQLite Databases (*.sqlite3 *.db)"); + } + + if (dbPath.isEmpty()) { + return 1; // User cancelled or still not found + } } - } - if (!DatabaseManager::instance().connect(dbPath)) { - QString errorMsg = - QString("Failed to connect to database at:\n%1\n\nPlease ensure the " - "database file exists or browse for a valid SQLite file.") - .arg(dbPath); - qCritical() << errorMsg; - QMessageBox::critical(nullptr, "Database Error", errorMsg); - // Let's try to let the user browse one more time before giving up - return 1; - } - qDebug() << "Connected to database at:" << dbPath; + if (!DatabaseManager::instance().connect(dbPath)) { + QString errorMsg = QString( + "Failed to connect to database at:\n%1\n\nPlease ensure the " + "database file exists or browse for a valid SQLite file.") + .arg(dbPath); + qCritical() << errorMsg; + QMessageBox::critical(nullptr, "Database Error", errorMsg); + // Let's try to let the user browse one more time before giving up + return 1; + } + qDebug() << "Connected to database at:" << dbPath; - MainWindow window; - window.show(); + MainWindow window; + window.show(); - return QApplication::exec(); + return QApplication::exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9c6d2e6..68af697 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6 +1,5 @@ #include "mainwindow.h" -#include "db/databasemanager.h" -#include "widgets/rdasettingswidget.h" + #include #include #include @@ -12,173 +11,168 @@ #include #include -MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { - for (int i = 0; i < MaxRecentFiles; ++i) { - recentFileActions[i] = new QAction(this); - recentFileActions[i]->setVisible(false); - connect(recentFileActions[i], &QAction::triggered, this, - &MainWindow::onRecentFileClick); - } - setupUi(); - updateRecentFileActions(); +#include "db/databasemanager.h" +#include "widgets/rdasettingswidget.h" + +MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { + for (int i = 0; i < MaxRecentFiles; ++i) { + recentFileActions[i] = new QAction(this); + recentFileActions[i]->setVisible(false); + connect(recentFileActions[i], &QAction::triggered, this, &MainWindow::onRecentFileClick); + } + setupUi(); + updateRecentFileActions(); } MainWindow::~MainWindow() = default; void MainWindow::setupUi() { - setWindowTitle("Nutrient Coach"); - setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); - resize(1000, 700); - - // File Menu - auto *fileMenu = menuBar()->addMenu("&File"); - auto *openDbAction = fileMenu->addAction("&Open Database..."); - recentFilesMenu = fileMenu->addMenu("Recent Databases"); - fileMenu->addSeparator(); - auto *exitAction = fileMenu->addAction("E&xit"); - connect(openDbAction, &QAction::triggered, this, &MainWindow::onOpenDatabase); - connect(exitAction, &QAction::triggered, this, &QWidget::close); - - for (int i = 0; i < MaxRecentFiles; ++i) - recentFilesMenu->addAction(recentFileActions[i]); - - // Edit Menu - QMenu *editMenu = menuBar()->addMenu("Edit"); - QAction *rdaAction = editMenu->addAction("RDA Settings"); - connect(rdaAction, &QAction::triggered, this, [this]() { - RDASettingsWidget dlg(repository, this); - dlg.exec(); - }); - - QAction *settingsAction = editMenu->addAction("Settings"); - connect(settingsAction, &QAction::triggered, this, &MainWindow::onSettings); - - // Help Menu - auto *helpMenu = menuBar()->addMenu("&Help"); - auto *aboutAction = helpMenu->addAction("&About"); - connect(aboutAction, &QAction::triggered, this, &MainWindow::onAbout); - - auto *centralWidget = new QWidget(this); - setCentralWidget(centralWidget); - - auto *mainLayout = new QVBoxLayout(centralWidget); - - tabs = new QTabWidget(this); - mainLayout->addWidget(tabs); - - // Search Tab - searchWidget = new SearchWidget(this); - tabs->addTab(searchWidget, "Search Foods"); - - // Connect signal - connect(searchWidget, &SearchWidget::foodSelected, this, - [=](int foodId, const QString &foodName) { - qDebug() << "Selected food:" << foodName << "(" << foodId << ")"; - detailsWidget->loadFood(foodId, foodName); - tabs->setCurrentWidget(detailsWidget); - }); - - connect(searchWidget, &SearchWidget::addToMealRequested, this, - [=](int foodId, const QString &foodName, double grams) { - mealWidget->addFood(foodId, foodName, grams); - tabs->setCurrentWidget(mealWidget); - }); - - // Analysis Tab - detailsWidget = new DetailsWidget(this); - tabs->addTab(detailsWidget, "Analyze"); - - // Meal Tab - mealWidget = new MealWidget(this); - tabs->addTab(mealWidget, "Meal Tracker"); - - // Connect Analysis -> Meal - connect(detailsWidget, &DetailsWidget::addToMeal, this, - [=](int foodId, const QString &foodName, double grams) { - mealWidget->addFood(foodId, foodName, grams); - // Optional: switch tab? - // tabs->setCurrentWidget(mealWidget); - }); + setWindowTitle("Nutrient Coach"); + setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png")); + resize(1000, 700); + + // File Menu + auto* fileMenu = menuBar()->addMenu("&File"); + auto* openDbAction = fileMenu->addAction("&Open Database..."); + recentFilesMenu = fileMenu->addMenu("Recent Databases"); + fileMenu->addSeparator(); + auto* exitAction = fileMenu->addAction("E&xit"); + connect(openDbAction, &QAction::triggered, this, &MainWindow::onOpenDatabase); + connect(exitAction, &QAction::triggered, this, &QWidget::close); + + for (int i = 0; i < MaxRecentFiles; ++i) recentFilesMenu->addAction(recentFileActions[i]); + + // Edit Menu + QMenu* editMenu = menuBar()->addMenu("Edit"); + QAction* rdaAction = editMenu->addAction("RDA Settings"); + connect(rdaAction, &QAction::triggered, this, [this]() { + RDASettingsWidget dlg(repository, this); + dlg.exec(); + }); + + QAction* settingsAction = editMenu->addAction("Settings"); + connect(settingsAction, &QAction::triggered, this, &MainWindow::onSettings); + + // Help Menu + auto* helpMenu = menuBar()->addMenu("&Help"); + auto* aboutAction = helpMenu->addAction("&About"); + connect(aboutAction, &QAction::triggered, this, &MainWindow::onAbout); + + auto* centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + + auto* mainLayout = new QVBoxLayout(centralWidget); + + tabs = new QTabWidget(this); + mainLayout->addWidget(tabs); + + // Search Tab + searchWidget = new SearchWidget(this); + tabs->addTab(searchWidget, "Search Foods"); + + // Connect signal + connect(searchWidget, &SearchWidget::foodSelected, this, + [=](int foodId, const QString& foodName) { + qDebug() << "Selected food:" << foodName << "(" << foodId << ")"; + detailsWidget->loadFood(foodId, foodName); + tabs->setCurrentWidget(detailsWidget); + }); + + connect(searchWidget, &SearchWidget::addToMealRequested, this, + [=](int foodId, const QString& foodName, double grams) { + mealWidget->addFood(foodId, foodName, grams); + tabs->setCurrentWidget(mealWidget); + }); + + // Analysis Tab + detailsWidget = new DetailsWidget(this); + tabs->addTab(detailsWidget, "Analyze"); + + // Meal Tab + mealWidget = new MealWidget(this); + tabs->addTab(mealWidget, "Meal Tracker"); + + // Connect Analysis -> Meal + connect(detailsWidget, &DetailsWidget::addToMeal, this, + [=](int foodId, const QString& foodName, double grams) { + mealWidget->addFood(foodId, foodName, grams); + // Optional: switch tab? + // tabs->setCurrentWidget(mealWidget); + }); } void MainWindow::onOpenDatabase() { - QString fileName = QFileDialog::getOpenFileName( - this, "Open USDA Database", QDir::homePath() + "/.nutra", - "SQLite Databases (*.sqlite3 *.db)"); - - if (!fileName.isEmpty()) { - if (DatabaseManager::instance().connect(fileName)) { - qDebug() << "Switched to database:" << fileName; - addToRecentFiles(fileName); - // In a real app, we'd emit a signal or refresh all widgets - // For now, let's just log and show success - QMessageBox::information(this, "Database Opened", - "Successfully connected to: " + fileName); - } else { - QMessageBox::critical(this, "Database Error", - "Failed to connect to the database."); + QString fileName = + QFileDialog::getOpenFileName(this, "Open USDA Database", QDir::homePath() + "/.nutra", + "SQLite Databases (*.sqlite3 *.db)"); + + if (!fileName.isEmpty()) { + if (DatabaseManager::instance().connect(fileName)) { + qDebug() << "Switched to database:" << fileName; + addToRecentFiles(fileName); + // In a real app, we'd emit a signal or refresh all widgets + // For now, let's just log and show success + QMessageBox::information(this, "Database Opened", + "Successfully connected to: " + fileName); + } else { + QMessageBox::critical(this, "Database Error", "Failed to connect to the database."); + } } - } } void MainWindow::onRecentFileClick() { - auto *action = qobject_cast(sender()); - if (action != nullptr) { - QString fileName = action->data().toString(); - if (DatabaseManager::instance().connect(fileName)) { - qDebug() << "Switched to database (recent):" << fileName; - addToRecentFiles(fileName); - QMessageBox::information(this, "Database Opened", - "Successfully connected to: " + fileName); - } else { - QMessageBox::critical(this, "Database Error", - "Failed to connect to: " + fileName); + auto* action = qobject_cast(sender()); + if (action != nullptr) { + QString fileName = action->data().toString(); + if (DatabaseManager::instance().connect(fileName)) { + qDebug() << "Switched to database (recent):" << fileName; + addToRecentFiles(fileName); + QMessageBox::information(this, "Database Opened", + "Successfully connected to: " + fileName); + } else { + QMessageBox::critical(this, "Database Error", "Failed to connect to: " + fileName); + } } - } } void MainWindow::updateRecentFileActions() { - QSettings settings("NutraTech", "Nutra"); - QStringList files = settings.value("recentFiles").toStringList(); - - int numRecentFiles = qMin(files.size(), MaxRecentFiles); - - for (int i = 0; i < numRecentFiles; ++i) { - QString text = - QString("&%1 %2").arg(i + 1).arg(QFileInfo(files[i]).fileName()); - recentFileActions[i]->setText(text); - recentFileActions[i]->setData(files[i]); - recentFileActions[i]->setVisible(true); - } - for (int i = numRecentFiles; i < MaxRecentFiles; ++i) - recentFileActions[i]->setVisible(false); - - recentFilesMenu->setEnabled(numRecentFiles > 0); + QSettings settings("NutraTech", "Nutra"); + QStringList files = settings.value("recentFiles").toStringList(); + + int numRecentFiles = qMin(files.size(), MaxRecentFiles); + + for (int i = 0; i < numRecentFiles; ++i) { + QString text = QString("&%1 %2").arg(i + 1).arg(QFileInfo(files[i]).fileName()); + recentFileActions[i]->setText(text); + recentFileActions[i]->setData(files[i]); + recentFileActions[i]->setVisible(true); + } + for (int i = numRecentFiles; i < MaxRecentFiles; ++i) recentFileActions[i]->setVisible(false); + + recentFilesMenu->setEnabled(numRecentFiles > 0); } -void MainWindow::addToRecentFiles(const QString &path) { - QSettings settings("NutraTech", "Nutra"); - QStringList files = settings.value("recentFiles").toStringList(); - files.removeAll(path); - files.prepend(path); - while (files.size() > MaxRecentFiles) - files.removeLast(); +void MainWindow::addToRecentFiles(const QString& path) { + QSettings settings("NutraTech", "Nutra"); + QStringList files = settings.value("recentFiles").toStringList(); + files.removeAll(path); + files.prepend(path); + while (files.size() > MaxRecentFiles) files.removeLast(); - settings.setValue("recentFiles", files); - updateRecentFileActions(); + settings.setValue("recentFiles", files); + updateRecentFileActions(); } void MainWindow::onSettings() { - QMessageBox::information(this, "Settings", "Settings dialog coming soon!"); + QMessageBox::information(this, "Settings", "Settings dialog coming soon!"); } void MainWindow::onAbout() { - QMessageBox::about( - this, "About Nutrient Coach", - QString("

Nutrient Coach %1

" - "

A C++/Qt application for tracking nutrition.

" - "

Homepage: https://github.com/" - "nutratech/gui

") - .arg(NUTRA_VERSION_STRING)); + QMessageBox::about(this, "About Nutrient Coach", + QString("

Nutrient Coach %1

" + "

A C++/Qt application for tracking nutrition.

" + "

Homepage: https://github.com/" + "nutratech/gui

") + .arg(NUTRA_VERSION_STRING)); } diff --git a/src/utils/string_utils.cpp b/src/utils/string_utils.cpp index a6d1635..4c7bf21 100644 --- a/src/utils/string_utils.cpp +++ b/src/utils/string_utils.cpp @@ -1,112 +1,110 @@ #include "utils/string_utils.h" + #include #include #include -#include // Required for std::max +#include // Required for std::max #include #ifndef QT_VERSION_CHECK -#define QT_VERSION_CHECK(major, minor, patch) \ - ((major << 16) | (minor << 8) | (patch)) +#define QT_VERSION_CHECK(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) #endif namespace Utils { -int levenshteinDistance(const QString &s1, const QString &s2) { - const auto m = s1.length(); - const auto n = s2.length(); - - std::vector> dp(m + 1, std::vector(n + 1)); - - for (int i = 0; i <= m; ++i) { - dp[i][0] = i; - } - for (int j = 0; j <= n; ++j) { - dp[0][j] = j; - } - - for (int i = 1; i <= m; ++i) { - for (int j = 1; j <= n; ++j) { - if (s1[i - 1] == s2[j - 1]) { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); - } +int levenshteinDistance(const QString& s1, const QString& s2) { + const auto m = s1.length(); + const auto n = s2.length(); + + std::vector> dp(m + 1, std::vector(n + 1)); + + for (int i = 0; i <= m; ++i) { + dp[i][0] = i; + } + for (int j = 0; j <= n; ++j) { + dp[0][j] = j; + } + + for (int i = 1; i <= m; ++i) { + for (int j = 1; j <= n; ++j) { + if (s1[i - 1] == s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + std::min({dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]}); + } + } } - } - return dp[m][n]; + return dp[m][n]; } -int calculateFuzzyScore(const QString &query, const QString &target) { - if (query.isEmpty()) { - return 0; - } - if (target.isEmpty()) { - return 0; - } - - QString q = query.toLower(); - QString t = target.toLower(); - - // 1. Exact match bonus - if (t == q) { - return 100; - } - - // 2. Contains match bonus (very strong signal) - if (t.contains(q)) { - return 90; // Base score for containing the string - } - - // 3. Token-based matching (handling "grass fed" vs "beef, grass-fed") - static const QRegularExpression regex("[\\s,-]+"); +int calculateFuzzyScore(const QString& query, const QString& target) { + if (query.isEmpty()) { + return 0; + } + if (target.isEmpty()) { + return 0; + } + + QString q = query.toLower(); + QString t = target.toLower(); + + // 1. Exact match bonus + if (t == q) { + return 100; + } + + // 2. Contains match bonus (very strong signal) + if (t.contains(q)) { + return 90; // Base score for containing the string + } + + // 3. Token-based matching (handling "grass fed" vs "beef, grass-fed") + static const QRegularExpression regex("[\\s,-]+"); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto behavior = Qt::SkipEmptyParts; + auto behavior = Qt::SkipEmptyParts; #else - auto behavior = QString::SkipEmptyParts; + auto behavior = QString::SkipEmptyParts; #endif - QStringList queryTokens = q.split(regex, behavior); - QStringList targetTokens = t.split(regex, behavior); - - int totalScore = 0; - int matchedTokens = 0; - - for (const QString &qToken : queryTokens) { - int maxTokenScore = 0; - for (const QString &tToken : targetTokens) { - int dist = levenshteinDistance(qToken, tToken); - int maxLen = static_cast(std::max(qToken.length(), tToken.length())); - if (maxLen == 0) - continue; - - int score = 0; - if (tToken.startsWith(qToken)) { - score = 95; // Prefix match is very good - } else { - double ratio = 1.0 - (static_cast(dist) / maxLen); - score = static_cast(ratio * 100); - } - - maxTokenScore = std::max(maxTokenScore, score); + QStringList queryTokens = q.split(regex, behavior); + QStringList targetTokens = t.split(regex, behavior); + + int totalScore = 0; + int matchedTokens = 0; + + for (const QString& qToken : queryTokens) { + int maxTokenScore = 0; + for (const QString& tToken : targetTokens) { + int dist = levenshteinDistance(qToken, tToken); + int maxLen = static_cast(std::max(qToken.length(), tToken.length())); + if (maxLen == 0) continue; + + int score = 0; + if (tToken.startsWith(qToken)) { + score = 95; // Prefix match is very good + } else { + double ratio = 1.0 - (static_cast(dist) / maxLen); + score = static_cast(ratio * 100); + } + + maxTokenScore = std::max(maxTokenScore, score); + } + totalScore += maxTokenScore; + if (maxTokenScore > 60) matchedTokens++; } - totalScore += maxTokenScore; - if (maxTokenScore > 60) - matchedTokens++; - } - if (queryTokens.isEmpty()) { - return 0; - } + if (queryTokens.isEmpty()) { + return 0; + } - int averageScore = static_cast(totalScore / queryTokens.size()); + int averageScore = static_cast(totalScore / queryTokens.size()); - // Penalize if not all tokens matched somewhat well - if (matchedTokens < queryTokens.size()) { - averageScore -= 20; - } + // Penalize if not all tokens matched somewhat well + if (matchedTokens < queryTokens.size()) { + averageScore -= 20; + } - return std::max(0, averageScore); + return std::max(0, averageScore); } -} // namespace Utils +} // namespace Utils diff --git a/src/widgets/detailswidget.cpp b/src/widgets/detailswidget.cpp index c775d05..d3041df 100644 --- a/src/widgets/detailswidget.cpp +++ b/src/widgets/detailswidget.cpp @@ -1,63 +1,61 @@ #include "widgets/detailswidget.h" + #include #include #include #include -DetailsWidget::DetailsWidget(QWidget *parent) - : QWidget(parent), currentFoodId(-1) { - auto *layout = new QVBoxLayout(this); - - // Header - auto *headerLayout = new QHBoxLayout(); - nameLabel = new QLabel("No food selected", this); - QFont font = nameLabel->font(); - font.setPointSize(14); - font.setBold(true); - nameLabel->setFont(font); - - addButton = new QPushButton("Add to Meal", this); - addButton->setEnabled(false); - connect(addButton, &QPushButton::clicked, this, &DetailsWidget::onAddClicked); - - headerLayout->addWidget(nameLabel); - headerLayout->addStretch(); - headerLayout->addWidget(addButton); - layout->addLayout(headerLayout); - - // Nutrients Table - nutrientsTable = new QTableWidget(this); - nutrientsTable->setColumnCount(3); - nutrientsTable->setHorizontalHeaderLabels({"Nutrient", "Amount", "Unit"}); - nutrientsTable->horizontalHeader()->setSectionResizeMode( - 0, QHeaderView::Stretch); - nutrientsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - layout->addWidget(nutrientsTable); +DetailsWidget::DetailsWidget(QWidget* parent) : QWidget(parent), currentFoodId(-1) { + auto* layout = new QVBoxLayout(this); + + // Header + auto* headerLayout = new QHBoxLayout(); + nameLabel = new QLabel("No food selected", this); + QFont font = nameLabel->font(); + font.setPointSize(14); + font.setBold(true); + nameLabel->setFont(font); + + addButton = new QPushButton("Add to Meal", this); + addButton->setEnabled(false); + connect(addButton, &QPushButton::clicked, this, &DetailsWidget::onAddClicked); + + headerLayout->addWidget(nameLabel); + headerLayout->addStretch(); + headerLayout->addWidget(addButton); + layout->addLayout(headerLayout); + + // Nutrients Table + nutrientsTable = new QTableWidget(this); + nutrientsTable->setColumnCount(3); + nutrientsTable->setHorizontalHeaderLabels({"Nutrient", "Amount", "Unit"}); + nutrientsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + nutrientsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + layout->addWidget(nutrientsTable); } -void DetailsWidget::loadFood(int foodId, const QString &foodName) { - currentFoodId = foodId; - currentFoodName = foodName; - nameLabel->setText(foodName + QString(" (ID: %1)").arg(foodId)); - addButton->setEnabled(true); +void DetailsWidget::loadFood(int foodId, const QString& foodName) { + currentFoodId = foodId; + currentFoodName = foodName; + nameLabel->setText(foodName + QString(" (ID: %1)").arg(foodId)); + addButton->setEnabled(true); - nutrientsTable->setRowCount(0); + nutrientsTable->setRowCount(0); - std::vector nutrients = repository.getFoodNutrients(foodId); + std::vector nutrients = repository.getFoodNutrients(foodId); - nutrientsTable->setRowCount(static_cast(nutrients.size())); - for (int i = 0; i < static_cast(nutrients.size()); ++i) { - const auto &nut = nutrients[i]; - nutrientsTable->setItem(i, 0, new QTableWidgetItem(nut.description)); - nutrientsTable->setItem(i, 1, - new QTableWidgetItem(QString::number(nut.amount))); - nutrientsTable->setItem(i, 2, new QTableWidgetItem(nut.unit)); - } + nutrientsTable->setRowCount(static_cast(nutrients.size())); + for (int i = 0; i < static_cast(nutrients.size()); ++i) { + const auto& nut = nutrients[i]; + nutrientsTable->setItem(i, 0, new QTableWidgetItem(nut.description)); + nutrientsTable->setItem(i, 1, new QTableWidgetItem(QString::number(nut.amount))); + nutrientsTable->setItem(i, 2, new QTableWidgetItem(nut.unit)); + } } void DetailsWidget::onAddClicked() { - if (currentFoodId != -1) { - // Default 100g - emit addToMeal(currentFoodId, currentFoodName, 100.0); - } + if (currentFoodId != -1) { + // Default 100g + emit addToMeal(currentFoodId, currentFoodName, 100.0); + } } diff --git a/src/widgets/mealwidget.cpp b/src/widgets/mealwidget.cpp index 9078ebd..2921932 100644 --- a/src/widgets/mealwidget.cpp +++ b/src/widgets/mealwidget.cpp @@ -1,98 +1,109 @@ #include "widgets/mealwidget.h" + #include #include #include #include #include -MealWidget::MealWidget(QWidget *parent) : QWidget(parent) { - auto *layout = new QVBoxLayout(this); - - // Items List - layout->addWidget(new QLabel("Meal Composition", this)); - itemsTable = new QTableWidget(this); - itemsTable->setColumnCount(3); - itemsTable->setHorizontalHeaderLabels({"Food", "Grams", "Calories"}); - itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); - layout->addWidget(itemsTable); - - // Controls - clearButton = new QPushButton("Clear Meal", this); - connect(clearButton, &QPushButton::clicked, this, &MealWidget::clearMeal); - layout->addWidget(clearButton); - - // Totals - layout->addWidget(new QLabel("Total Nutrition", this)); - totalsTable = new QTableWidget(this); - totalsTable->setColumnCount(3); - totalsTable->setHorizontalHeaderLabels({"Nutrient", "Total", "Unit"}); - totalsTable->horizontalHeader()->setSectionResizeMode(0, - QHeaderView::Stretch); - layout->addWidget(totalsTable); +MealWidget::MealWidget(QWidget* parent) : QWidget(parent) { + auto* layout = new QVBoxLayout(this); + + // Items List + layout->addWidget(new QLabel("Meal Composition", this)); + itemsTable = new QTableWidget(this); + itemsTable->setColumnCount(3); + itemsTable->setHorizontalHeaderLabels({"Food", "Grams", "Calories"}); + itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + layout->addWidget(itemsTable); + + // Controls + clearButton = new QPushButton("Clear Meal", this); + connect(clearButton, &QPushButton::clicked, this, &MealWidget::clearMeal); + layout->addWidget(clearButton); + + // Totals + layout->addWidget(new QLabel("Total Nutrition", this)); + totalsTable = new QTableWidget(this); + totalsTable->setColumnCount(3); + totalsTable->setHorizontalHeaderLabels({"Nutrient", "Total", "Unit"}); + totalsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + layout->addWidget(totalsTable); + + refresh(); // Load initial state +} + +void MealWidget::addFood(int foodId, const QString& foodName, double grams) { + // Default to meal_id 1 (e.g. Breakfast/General) for now + // TODO: Add UI to select meal + m_mealRepo.addFoodLog(foodId, grams, 1); + refresh(); } -void MealWidget::addFood(int foodId, const QString &foodName, double grams) { - std::vector baseNutrients = repository.getFoodNutrients(foodId); - - MealItem item; - item.foodId = foodId; - item.name = foodName; - item.grams = grams; - item.nutrients_100g = baseNutrients; - - mealItems.push_back(item); - - // Update Items Table - int row = itemsTable->rowCount(); - itemsTable->insertRow(row); - itemsTable->setItem(row, 0, new QTableWidgetItem(foodName)); - itemsTable->setItem(row, 1, new QTableWidgetItem(QString::number(grams))); - - // Calculate Calories (ID 208 usually, or find by name?) - // repository returns IDs based on DB. 208 is KCAL in SR28. - double kcal = 0; - for (const auto &nut : baseNutrients) { - if (nut.id == 208) { - kcal = (nut.amount * grams) / 100.0; - break; +void MealWidget::refresh() { + mealItems.clear(); + itemsTable->setRowCount(0); + + auto logs = m_mealRepo.getDailyLogs(); // defaults to today + + for (const auto& log : logs) { + std::vector baseNutrients = repository.getFoodNutrients(log.foodId); + + MealItem item; + item.foodId = log.foodId; + item.name = log.foodName; + item.grams = log.grams; + item.nutrients_100g = baseNutrients; + mealItems.push_back(item); + + // Update Items Table + int row = itemsTable->rowCount(); + itemsTable->insertRow(row); + itemsTable->setItem(row, 0, new QTableWidgetItem(item.name)); + itemsTable->setItem(row, 1, new QTableWidgetItem(QString::number(item.grams))); + + // Calculate Calories (ID 208) + double kcal = 0; + for (const auto& nut : baseNutrients) { + if (nut.id == 208) { + kcal = (nut.amount * item.grams) / 100.0; + break; + } + } + itemsTable->setItem(row, 2, new QTableWidgetItem(QString::number(kcal, 'f', 1))); } - } - itemsTable->setItem(row, 2, - new QTableWidgetItem(QString::number(kcal, 'f', 1))); - updateTotals(); + updateTotals(); } void MealWidget::clearMeal() { - mealItems.clear(); - itemsTable->setRowCount(0); - updateTotals(); + m_mealRepo.clearDailyLogs(); + refresh(); } void MealWidget::updateTotals() { - std::map totals; // id -> amount - std::map units; - std::map names; - - for (const auto &item : mealItems) { - double scale = item.grams / 100.0; - for (const auto &nut : item.nutrients_100g) { - totals[nut.id] += nut.amount * scale; - names.try_emplace(nut.id, nut.description); - units.try_emplace(nut.id, nut.unit); + std::map totals; // id -> amount + std::map units; + std::map names; + + for (const auto& item : mealItems) { + double scale = item.grams / 100.0; + for (const auto& nut : item.nutrients_100g) { + totals[nut.id] += nut.amount * scale; + names.try_emplace(nut.id, nut.description); + units.try_emplace(nut.id, nut.unit); + } + } + + totalsTable->setRowCount(static_cast(totals.size())); + int row = 0; + for (const auto& pair : totals) { + int nid = pair.first; + double amount = pair.second; + + totalsTable->setItem(row, 0, new QTableWidgetItem(names[nid])); + totalsTable->setItem(row, 1, new QTableWidgetItem(QString::number(amount, 'f', 2))); + totalsTable->setItem(row, 2, new QTableWidgetItem(units[nid])); + row++; } - } - - totalsTable->setRowCount(static_cast(totals.size())); - int row = 0; - for (const auto &pair : totals) { - int nid = pair.first; - double amount = pair.second; - - totalsTable->setItem(row, 0, new QTableWidgetItem(names[nid])); - totalsTable->setItem(row, 1, - new QTableWidgetItem(QString::number(amount, 'f', 2))); - totalsTable->setItem(row, 2, new QTableWidgetItem(units[nid])); - row++; - } } diff --git a/src/widgets/rdasettingswidget.cpp b/src/widgets/rdasettingswidget.cpp index 877fff3..251ebac 100644 --- a/src/widgets/rdasettingswidget.cpp +++ b/src/widgets/rdasettingswidget.cpp @@ -1,84 +1,81 @@ #include "widgets/rdasettingswidget.h" -#include "db/databasemanager.h" + #include #include #include #include -RDASettingsWidget::RDASettingsWidget(FoodRepository &repository, - QWidget *parent) +#include "db/databasemanager.h" + +RDASettingsWidget::RDASettingsWidget(FoodRepository& repository, QWidget* parent) : QDialog(parent), m_repository(repository) { - setWindowTitle("RDA Settings"); - resize(600, 400); + setWindowTitle("RDA Settings"); + resize(600, 400); - auto *layout = new QVBoxLayout(this); - layout->addWidget(new QLabel("Customize your Recommended Daily Allowances " - "(RDA). Changes are saved automatically.")); + auto* layout = new QVBoxLayout(this); + layout->addWidget( + new QLabel("Customize your Recommended Daily Allowances " + "(RDA). Changes are saved automatically.")); - m_table = new QTableWidget(this); - m_table->setColumnCount(4); - m_table->setHorizontalHeaderLabels({"ID", "Nutrient", "RDA", "Unit"}); - m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); - m_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_table = new QTableWidget(this); + m_table->setColumnCount(4); + m_table->setHorizontalHeaderLabels({"ID", "Nutrient", "RDA", "Unit"}); + m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_table->setSelectionBehavior(QAbstractItemView::SelectRows); - loadData(); + loadData(); - connect(m_table, &QTableWidget::cellChanged, this, - &RDASettingsWidget::onCellChanged); + connect(m_table, &QTableWidget::cellChanged, this, &RDASettingsWidget::onCellChanged); - layout->addWidget(m_table); + layout->addWidget(m_table); } void RDASettingsWidget::loadData() { - m_loading = true; - m_table->setRowCount(0); + m_loading = true; + m_table->setRowCount(0); - QSqlDatabase db = DatabaseManager::instance().database(); - if (!db.isOpen()) - return; + QSqlDatabase db = DatabaseManager::instance().database(); + if (!db.isOpen()) return; - // Get metadata from USDA - QSqlQuery query( - "SELECT id, nutr_desc, unit FROM nutrients_overview ORDER BY nutr_desc", - db); - auto currentRdas = m_repository.getNutrientRdas(); + // Get metadata from USDA + QSqlQuery query("SELECT id, nutr_desc, unit FROM nutrients_overview ORDER BY nutr_desc", db); + auto currentRdas = m_repository.getNutrientRdas(); - int row = 0; - while (query.next()) { - int id = query.value(0).toInt(); - QString name = query.value(1).toString(); - QString unit = query.value(2).toString(); - double rda = currentRdas.count(id) != 0U ? currentRdas[id] : 0.0; + int row = 0; + while (query.next()) { + int id = query.value(0).toInt(); + QString name = query.value(1).toString(); + QString unit = query.value(2).toString(); + double rda = currentRdas.count(id) != 0U ? currentRdas[id] : 0.0; - m_table->insertRow(row); + m_table->insertRow(row); - auto *idItem = new QTableWidgetItem(QString::number(id)); - idItem->setFlags(idItem->flags() & ~Qt::ItemIsEditable); - m_table->setItem(row, 0, idItem); + auto* idItem = new QTableWidgetItem(QString::number(id)); + idItem->setFlags(idItem->flags() & ~Qt::ItemIsEditable); + m_table->setItem(row, 0, idItem); - auto *nameItem = new QTableWidgetItem(name); - nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); - m_table->setItem(row, 1, nameItem); + auto* nameItem = new QTableWidgetItem(name); + nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); + m_table->setItem(row, 1, nameItem); - auto *rdaItem = new QTableWidgetItem(QString::number(rda)); - m_table->setItem(row, 2, rdaItem); + auto* rdaItem = new QTableWidgetItem(QString::number(rda)); + m_table->setItem(row, 2, rdaItem); - auto *unitItem = new QTableWidgetItem(unit); - unitItem->setFlags(unitItem->flags() & ~Qt::ItemIsEditable); - m_table->setItem(row, 3, unitItem); + auto* unitItem = new QTableWidgetItem(unit); + unitItem->setFlags(unitItem->flags() & ~Qt::ItemIsEditable); + m_table->setItem(row, 3, unitItem); - row++; - } + row++; + } - m_loading = false; + m_loading = false; } void RDASettingsWidget::onCellChanged(int row, int column) { - if (m_loading || column != 2) - return; + if (m_loading || column != 2) return; - int id = m_table->item(row, 0)->text().toInt(); - double value = m_table->item(row, 2)->text().toDouble(); + int id = m_table->item(row, 0)->text().toInt(); + double value = m_table->item(row, 2)->text().toDouble(); - m_repository.updateRda(id, value); + m_repository.updateRda(id, value); } diff --git a/src/widgets/searchwidget.cpp b/src/widgets/searchwidget.cpp index 37193c6..8168310 100644 --- a/src/widgets/searchwidget.cpp +++ b/src/widgets/searchwidget.cpp @@ -1,5 +1,5 @@ #include "widgets/searchwidget.h" -#include "widgets/weightinputdialog.h" + #include #include #include @@ -7,118 +7,108 @@ #include #include -SearchWidget::SearchWidget(QWidget *parent) : QWidget(parent) { - auto *layout = new QVBoxLayout(this); - - // Search bar - auto *searchLayout = new QHBoxLayout(); - searchInput = new QLineEdit(this); - searchInput->setPlaceholderText("Search for food..."); - - searchTimer = new QTimer(this); - searchTimer->setSingleShot(true); - searchTimer->setInterval(600); // 600ms debounce - - connect(searchInput, &QLineEdit::textChanged, this, - [=]() { searchTimer->start(); }); - connect(searchTimer, &QTimer::timeout, this, &SearchWidget::performSearch); - connect(searchInput, &QLineEdit::returnPressed, this, - &SearchWidget::performSearch); - - searchButton = new QPushButton("Search", this); - connect(searchButton, &QPushButton::clicked, this, - &SearchWidget::performSearch); - - searchLayout->addWidget(searchInput); - searchLayout->addWidget(searchButton); - layout->addLayout(searchLayout); - - // Results table - resultsTable = new QTableWidget(this); - resultsTable->setColumnCount(7); - resultsTable->setHorizontalHeaderLabels( - {"ID", "Description", "Group", "Nutr", "Amino", "Flav", "Score"}); - - resultsTable->horizontalHeader()->setSectionResizeMode(1, - QHeaderView::Stretch); - resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); - resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); - resultsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); - resultsTable->setContextMenuPolicy(Qt::CustomContextMenu); - - connect(resultsTable, &QTableWidget::cellDoubleClicked, this, - &SearchWidget::onRowDoubleClicked); - connect(resultsTable, &QTableWidget::customContextMenuRequested, this, - &SearchWidget::onCustomContextMenu); - - layout->addWidget(resultsTable); +#include "widgets/weightinputdialog.h" + +SearchWidget::SearchWidget(QWidget* parent) : QWidget(parent) { + auto* layout = new QVBoxLayout(this); + + // Search bar + auto* searchLayout = new QHBoxLayout(); + searchInput = new QLineEdit(this); + searchInput->setPlaceholderText("Search for food..."); + + searchTimer = new QTimer(this); + searchTimer->setSingleShot(true); + searchTimer->setInterval(600); // 600ms debounce + + connect(searchInput, &QLineEdit::textChanged, this, [=]() { searchTimer->start(); }); + connect(searchTimer, &QTimer::timeout, this, &SearchWidget::performSearch); + connect(searchInput, &QLineEdit::returnPressed, this, &SearchWidget::performSearch); + + searchButton = new QPushButton("Search", this); + connect(searchButton, &QPushButton::clicked, this, &SearchWidget::performSearch); + + searchLayout->addWidget(searchInput); + searchLayout->addWidget(searchButton); + layout->addLayout(searchLayout); + + // Results table + resultsTable = new QTableWidget(this); + resultsTable->setColumnCount(7); + resultsTable->setHorizontalHeaderLabels( + {"ID", "Description", "Group", "Nutr", "Amino", "Flav", "Score"}); + + resultsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + resultsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + resultsTable->setSelectionMode(QAbstractItemView::SingleSelection); + resultsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + resultsTable->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(resultsTable, &QTableWidget::cellDoubleClicked, this, + &SearchWidget::onRowDoubleClicked); + connect(resultsTable, &QTableWidget::customContextMenuRequested, this, + &SearchWidget::onCustomContextMenu); + + layout->addWidget(resultsTable); } void SearchWidget::performSearch() { - QString query = searchInput->text().trimmed(); - if (query.length() < 2) - return; - - resultsTable->setRowCount(0); - - std::vector results = repository.searchFoods(query); - - resultsTable->setRowCount(static_cast(results.size())); - for (int i = 0; i < static_cast(results.size()); ++i) { - const auto &item = results[i]; - resultsTable->setItem(i, 0, new QTableWidgetItem(QString::number(item.id))); - resultsTable->setItem(i, 1, new QTableWidgetItem(item.description)); - resultsTable->setItem(i, 2, new QTableWidgetItem(item.foodGroupName)); - resultsTable->setItem( - i, 3, new QTableWidgetItem(QString::number(item.nutrientCount))); - resultsTable->setItem( - i, 4, new QTableWidgetItem(QString::number(item.aminoCount))); - resultsTable->setItem( - i, 5, new QTableWidgetItem(QString::number(item.flavCount))); - resultsTable->setItem(i, 6, - new QTableWidgetItem(QString::number(item.score))); - } + QString query = searchInput->text().trimmed(); + if (query.length() < 2) return; + + resultsTable->setRowCount(0); + + std::vector results = repository.searchFoods(query); + + resultsTable->setRowCount(static_cast(results.size())); + for (int i = 0; i < static_cast(results.size()); ++i) { + const auto& item = results[i]; + resultsTable->setItem(i, 0, new QTableWidgetItem(QString::number(item.id))); + resultsTable->setItem(i, 1, new QTableWidgetItem(item.description)); + resultsTable->setItem(i, 2, new QTableWidgetItem(item.foodGroupName)); + resultsTable->setItem(i, 3, new QTableWidgetItem(QString::number(item.nutrientCount))); + resultsTable->setItem(i, 4, new QTableWidgetItem(QString::number(item.aminoCount))); + resultsTable->setItem(i, 5, new QTableWidgetItem(QString::number(item.flavCount))); + resultsTable->setItem(i, 6, new QTableWidgetItem(QString::number(item.score))); + } } void SearchWidget::onRowDoubleClicked(int row, int column) { - Q_UNUSED(column); - QTableWidgetItem *idItem = resultsTable->item(row, 0); - QTableWidgetItem *descItem = resultsTable->item(row, 1); + Q_UNUSED(column); + QTableWidgetItem* idItem = resultsTable->item(row, 0); + QTableWidgetItem* descItem = resultsTable->item(row, 1); - if (idItem != nullptr && descItem != nullptr) { - emit foodSelected(idItem->text().toInt(), descItem->text()); - } + if (idItem != nullptr && descItem != nullptr) { + emit foodSelected(idItem->text().toInt(), descItem->text()); + } } -void SearchWidget::onCustomContextMenu(const QPoint &pos) { - QTableWidgetItem *item = resultsTable->itemAt(pos); - if (item == nullptr) - return; +void SearchWidget::onCustomContextMenu(const QPoint& pos) { + QTableWidgetItem* item = resultsTable->itemAt(pos); + if (item == nullptr) return; - int row = item->row(); - QTableWidgetItem *idItem = resultsTable->item(row, 0); - QTableWidgetItem *descItem = resultsTable->item(row, 1); + int row = item->row(); + QTableWidgetItem* idItem = resultsTable->item(row, 0); + QTableWidgetItem* descItem = resultsTable->item(row, 1); - if (idItem == nullptr || descItem == nullptr) - return; + if (idItem == nullptr || descItem == nullptr) return; - int foodId = idItem->text().toInt(); - QString foodName = descItem->text(); + int foodId = idItem->text().toInt(); + QString foodName = descItem->text(); - QMenu menu(this); - QAction *analyzeAction = menu.addAction("Analyze"); - QAction *addToMealAction = menu.addAction("Add to Meal"); + QMenu menu(this); + QAction* analyzeAction = menu.addAction("Analyze"); + QAction* addToMealAction = menu.addAction("Add to Meal"); - QAction *selectedAction = - menu.exec(resultsTable->viewport()->mapToGlobal(pos)); + QAction* selectedAction = menu.exec(resultsTable->viewport()->mapToGlobal(pos)); - if (selectedAction == analyzeAction) { - emit foodSelected(foodId, foodName); - } else if (selectedAction == addToMealAction) { - std::vector servings = repository.getFoodServings(foodId); - WeightInputDialog dlg(foodName, servings, this); - if (dlg.exec() == QDialog::Accepted) { - emit addToMealRequested(foodId, foodName, dlg.getGrams()); + if (selectedAction == analyzeAction) { + emit foodSelected(foodId, foodName); + } else if (selectedAction == addToMealAction) { + std::vector servings = repository.getFoodServings(foodId); + WeightInputDialog dlg(foodName, servings, this); + if (dlg.exec() == QDialog::Accepted) { + emit addToMealRequested(foodId, foodName, dlg.getGrams()); + } } - } } diff --git a/src/widgets/weightinputdialog.cpp b/src/widgets/weightinputdialog.cpp index b5e2b5d..d3e9127 100644 --- a/src/widgets/weightinputdialog.cpp +++ b/src/widgets/weightinputdialog.cpp @@ -1,63 +1,62 @@ #include "widgets/weightinputdialog.h" + #include #include #include -WeightInputDialog::WeightInputDialog(const QString &foodName, - const std::vector &servings, - QWidget *parent) +WeightInputDialog::WeightInputDialog(const QString& foodName, + const std::vector& servings, QWidget* parent) : QDialog(parent), m_servings(servings) { - setWindowTitle("Add to Meal - " + foodName); - auto *layout = new QVBoxLayout(this); - - layout->addWidget( - new QLabel("How much " + foodName + " are you adding?", this)); - - auto *inputLayout = new QHBoxLayout(); - amountSpinBox = new QDoubleSpinBox(this); - amountSpinBox->setRange(0.1, 10000.0); - amountSpinBox->setValue(1.0); - amountSpinBox->setDecimals(2); - - unitComboBox = new QComboBox(this); - unitComboBox->addItem("Grams (g)", 1.0); - unitComboBox->addItem("Ounces (oz)", GRAMS_PER_OZ); - unitComboBox->addItem("Pounds (lb)", GRAMS_PER_LB); - - for (const auto &sw : servings) { - unitComboBox->addItem(sw.description, sw.grams); - } - - // Default to Grams and set value to 100 if Grams is selected - unitComboBox->setCurrentIndex(0); - amountSpinBox->setValue(100.0); - - // Update value when unit changes? No, let's keep it simple. - // Usually 100g is a good default, but 1 serving might be better if available. - if (!servings.empty()) { - unitComboBox->setCurrentIndex(3); // First serving - amountSpinBox->setValue(1.0); - } - - inputLayout->addWidget(amountSpinBox); - inputLayout->addWidget(unitComboBox); - layout->addLayout(inputLayout); + setWindowTitle("Add to Meal - " + foodName); + auto* layout = new QVBoxLayout(this); - auto *buttonLayout = new QHBoxLayout(); - auto *okButton = new QPushButton("Add to Meal", this); - auto *cancelButton = new QPushButton("Cancel", this); + layout->addWidget(new QLabel("How much " + foodName + " are you adding?", this)); - connect(okButton, &QPushButton::clicked, this, &QDialog::accept); - connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); - - buttonLayout->addStretch(); - buttonLayout->addWidget(cancelButton); - buttonLayout->addWidget(okButton); - layout->addLayout(buttonLayout); + auto* inputLayout = new QHBoxLayout(); + amountSpinBox = new QDoubleSpinBox(this); + amountSpinBox->setRange(0.1, 10000.0); + amountSpinBox->setValue(1.0); + amountSpinBox->setDecimals(2); + + unitComboBox = new QComboBox(this); + unitComboBox->addItem("Grams (g)", 1.0); + unitComboBox->addItem("Ounces (oz)", GRAMS_PER_OZ); + unitComboBox->addItem("Pounds (lb)", GRAMS_PER_LB); + + for (const auto& sw : servings) { + unitComboBox->addItem(sw.description, sw.grams); + } + + // Default to Grams and set value to 100 if Grams is selected + unitComboBox->setCurrentIndex(0); + amountSpinBox->setValue(100.0); + + // Update value when unit changes? No, let's keep it simple. + // Usually 100g is a good default, but 1 serving might be better if available. + if (!servings.empty()) { + unitComboBox->setCurrentIndex(3); // First serving + amountSpinBox->setValue(1.0); + } + + inputLayout->addWidget(amountSpinBox); + inputLayout->addWidget(unitComboBox); + layout->addLayout(inputLayout); + + auto* buttonLayout = new QHBoxLayout(); + auto* okButton = new QPushButton("Add to Meal", this); + auto* cancelButton = new QPushButton("Cancel", this); + + connect(okButton, &QPushButton::clicked, this, &QDialog::accept); + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); + + buttonLayout->addStretch(); + buttonLayout->addWidget(cancelButton); + buttonLayout->addWidget(okButton); + layout->addLayout(buttonLayout); } double WeightInputDialog::getGrams() const { - double amount = amountSpinBox->value(); - double multiplier = unitComboBox->currentData().toDouble(); - return amount * multiplier; + double amount = amountSpinBox->value(); + double multiplier = unitComboBox->currentData().toDouble(); + return amount * multiplier; } diff --git a/tests/test_foodrepository.cpp b/tests/test_foodrepository.cpp index 60a11c2..c45d3ae 100644 --- a/tests/test_foodrepository.cpp +++ b/tests/test_foodrepository.cpp @@ -1,57 +1,56 @@ -#include "db/databasemanager.h" -#include "db/foodrepository.h" #include #include #include +#include "db/databasemanager.h" +#include "db/foodrepository.h" + class TestFoodRepository : public QObject { - Q_OBJECT + Q_OBJECT private slots: - void initTestCase() { - // Setup DB connection - // Allow override via environment variable for CI/testing - QString envPath = qgetenv("NUTRA_DB_PATH"); - QString dbPath = - envPath.isEmpty() ? QDir::homePath() + "/.nutra/usda.sqlite3" : envPath; - - if (!QFileInfo::exists(dbPath)) { - QSKIP("Database file not found (NUTRA_DB_PATH or ~/.nutra/usda.sqlite3). " - "Skipping DB tests."); + void initTestCase() { + // Setup DB connection + // Allow override via environment variable for CI/testing + QString envPath = qgetenv("NUTRA_DB_PATH"); + QString dbPath = envPath.isEmpty() ? QDir::homePath() + "/.nutra/usda.sqlite3" : envPath; + + if (!QFileInfo::exists(dbPath)) { + QSKIP( + "Database file not found (NUTRA_DB_PATH or ~/.nutra/usda.sqlite3). " + "Skipping DB tests."); + } + + bool connected = DatabaseManager::instance().connect(dbPath); + QVERIFY2(connected, "Failed to connect to database"); + } + + void testSearchFoods() { + FoodRepository repo; + auto results = repo.searchFoods("apple"); + QVERIFY2(!results.empty(), "Search should return results for 'apple'"); + bool found = false; + for (const auto& item : results) { + if (item.description.contains("Apple", Qt::CaseInsensitive)) { + found = true; + break; + } + } + QVERIFY2(found, "Search results should contain 'Apple'"); } - bool connected = DatabaseManager::instance().connect(dbPath); - QVERIFY2(connected, "Failed to connect to database"); - } - - void testSearchFoods() { - FoodRepository repo; - auto results = repo.searchFoods("apple"); - QVERIFY2(!results.empty(), "Search should return results for 'apple'"); - bool found = false; - for (const auto &item : results) { - if (item.description.contains("Apple", Qt::CaseInsensitive)) { - found = true; - break; - } + void testGetFoodNutrients() { + FoodRepository repo; + // Known ID for "Apples, raw, with skin" might be 9003 in SR28, but let's + // search first or pick a known one if we knew it. Let's just use the first + // result from search. + auto results = repo.searchFoods("apple"); + if (results.empty()) QSKIP("No foods found to test nutrients"); + + int foodId = results[0].id; + auto nutrients = repo.getFoodNutrients(foodId); + QVERIFY2(!nutrients.empty(), "Nutrients should not be empty for a valid food"); } - QVERIFY2(found, "Search results should contain 'Apple'"); - } - - void testGetFoodNutrients() { - FoodRepository repo; - // Known ID for "Apples, raw, with skin" might be 9003 in SR28, but let's - // search first or pick a known one if we knew it. Let's just use the first - // result from search. - auto results = repo.searchFoods("apple"); - if (results.empty()) - QSKIP("No foods found to test nutrients"); - - int foodId = results[0].id; - auto nutrients = repo.getFoodNutrients(foodId); - QVERIFY2(!nutrients.empty(), - "Nutrients should not be empty for a valid food"); - } }; QTEST_MAIN(TestFoodRepository) -- 2.52.0