]> Nutra Git (v2) - nutratech/gui.git/commitdiff
update
authorShane Jaroch <chown_tee@proton.me>
Thu, 22 Jan 2026 06:39:20 +0000 (01:39 -0500)
committerShane Jaroch <chown_tee@proton.me>
Thu, 22 Jan 2026 06:39:20 +0000 (01:39 -0500)
16 files changed:
CMakeLists.txt
include/db/reciperepository.h [new file with mode: 0644]
include/mainwindow.h
include/widgets/recipewidget.h [new file with mode: 0644]
lib/ntsqlite
src/db/databasemanager.cpp
src/db/reciperepository.cpp [new file with mode: 0644]
src/main.cpp
src/mainwindow.cpp
src/widgets/profilesettingswidget.cpp
src/widgets/recipewidget.cpp [new file with mode: 0644]
src/widgets/searchwidget.cpp
tests/test_calculations.cpp
tests/test_calculations.h [new file with mode: 0644]
tests/test_databasemanager.cpp
tests/test_databasemanager.h [new file with mode: 0644]

index f4c8dfb0b12c974957405193e0f78bef0ba70a04..07ea4d66b016c60d133def704c927ac2652e2113 100644 (file)
@@ -58,6 +58,7 @@ add_test(NAME FoodRepoTest COMMAND test_nutra)
 
 file(GLOB_RECURSE TEST_DB_SOURCES
     tests/test_databasemanager.cpp
+    tests/test_databasemanager.h
     src/db/*.cpp
     src/utils/*.cpp
 )
@@ -66,7 +67,7 @@ target_include_directories(test_databasemanager PRIVATE ${CMAKE_SOURCE_DIR}/incl
 target_link_libraries(test_databasemanager PRIVATE Qt${QT_VERSION_MAJOR}::Test Qt${QT_VERSION_MAJOR}::Sql)
 add_test(NAME DatabaseManagerTest COMMAND test_databasemanager)
 
-add_executable(test_calculations tests/test_calculations.cpp)
+add_executable(test_calculations tests/test_calculations.cpp tests/test_calculations.h)
 target_include_directories(test_calculations PRIVATE ${CMAKE_SOURCE_DIR}/include)
 target_link_libraries(test_calculations PRIVATE Qt${QT_VERSION_MAJOR}::Test)
 add_test(NAME CalculationsTest COMMAND test_calculations)
diff --git a/include/db/reciperepository.h b/include/db/reciperepository.h
new file mode 100644 (file)
index 0000000..57914e5
--- /dev/null
@@ -0,0 +1,43 @@
+#ifndef RECIPEREPOSITORY_H
+#define RECIPEREPOSITORY_H
+
+#include <QDateTime>
+#include <QString>
+#include <vector>
+
+struct RecipeItem {
+    int id;
+    QString uuid;
+    QString name;
+    QString instructions;
+    QDateTime created;
+    double totalCalories = 0.0;  // Calculated on the fly ideally
+};
+
+struct RecipeIngredient {
+    int foodId;
+    QString foodName;
+    double amount;  // grams
+    // Potential for more info (calories contribution etc)
+};
+
+class RecipeRepository {
+public:
+    RecipeRepository();
+
+    // CRUD
+    int createRecipe(const QString& name, const QString& instructions = "");
+    bool updateRecipe(int id, const QString& name, const QString& instructions);
+    bool deleteRecipe(int id);
+
+    std::vector<RecipeItem> getAllRecipes();
+    RecipeItem getRecipe(int id);
+
+    // Ingredients
+    bool addIngredient(int recipeId, int foodId, double amount);
+    bool removeIngredient(int recipeId, int foodId);
+    bool updateIngredient(int recipeId, int foodId, double amount);
+    std::vector<RecipeIngredient> getIngredients(int recipeId);
+};
+
+#endif  // RECIPEREPOSITORY_H
index 68f1524a78bee7547ce7a194281af440785d2275..192679c390a2e1f0b5b72bf558b8b07512bf6083 100644 (file)
@@ -9,6 +9,7 @@
 #include "widgets/dailylogwidget.h"
 #include "widgets/detailswidget.h"
 #include "widgets/mealwidget.h"
+#include "widgets/recipewidget.h"
 #include "widgets/searchwidget.h"
 
 class MainWindow : public QMainWindow {
@@ -33,6 +34,7 @@ private:
     SearchWidget* searchWidget;
     DetailsWidget* detailsWidget;
     MealWidget* mealWidget;
+    RecipeWidget* recipeWidget;
     DailyLogWidget* dailyLogWidget;
     FoodRepository repository;
 
diff --git a/include/widgets/recipewidget.h b/include/widgets/recipewidget.h
new file mode 100644 (file)
index 0000000..5ca58b4
--- /dev/null
@@ -0,0 +1,55 @@
+#ifndef RECIPEWIDGET_H
+#define RECIPEWIDGET_H
+
+#include <QLabel>
+#include <QLineEdit>
+#include <QListWidget>
+#include <QPushButton>
+#include <QTableWidget>
+#include <QTextEdit>
+#include <QWidget>
+
+#include "db/foodrepository.h"
+#include "db/reciperepository.h"
+
+class RecipeWidget : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit RecipeWidget(QWidget* parent = nullptr);
+
+signals:
+    void recipeSelected(int recipeId);
+
+private slots:
+    void onNewRecipe();
+    void onSaveRecipe();
+    void onDeleteRecipe();
+    void onRecipeListSelectionChanged();
+    void onAddIngredient();
+    void onRemoveIngredient();
+
+private:
+    void setupUi();
+    void loadRecipes();
+    void loadRecipeDetails(int recipeId);
+    void clearDetails();
+
+    RecipeRepository repository;
+    FoodRepository foodRepo;  // For ingredient search/lookup
+
+    QListWidget* recipeList;
+    QLineEdit* nameEdit;
+    QTableWidget* ingredientsTable;
+    QTextEdit* instructionsEdit;
+
+    QPushButton* saveButton;
+    QPushButton* deleteButton;
+    QPushButton* newButton;
+    QPushButton* addIngredientButton;
+    QPushButton* removeIngredientButton;
+
+    int currentRecipeId = -1;
+};
+
+#endif  // RECIPEWIDGET_H
index acd5af5d0d87f7683086788ebcba94197cb5b660..97934ada10143640d4a3aa326cf1c9c8f245c65a 160000 (submodule)
@@ -1 +1 @@
-Subproject commit acd5af5d0d87f7683086788ebcba94197cb5b660
+Subproject commit 97934ada10143640d4a3aa326cf1c9c8f245c65a
index adab27539a09b7916308d47d57bcc382b6368582..a957a1678219f1d61f3b3d612cc48d2904f5e630 100644 (file)
@@ -146,18 +146,26 @@ void DatabaseManager::initUserDatabase() {
         QString schemaPath = QDir::currentPath() + "/lib/ntsqlite/sql/tables.sql";
         if (!QFileInfo::exists(schemaPath)) {
             // Fallback for installed location
-            schemaPath = "/usr/share/nutra/sql/tables.sql";
+            QString fallbackPath = "/usr/share/nutra/sql/tables.sql";
+            if (QFileInfo::exists(fallbackPath)) {
+                schemaPath = fallbackPath;
+            } else {
+                qCritical() << "Schema file not found at:" << schemaPath << "or" << fallbackPath;
+                return;
+            }
         }
         applySchema(query, schemaPath);
-    } else {
-        // Migration logic would go here
     }
 }
 
 void DatabaseManager::applySchema(QSqlQuery& query, const QString& schemaPath) {
+    if (!QFileInfo::exists(schemaPath)) {
+        qCritical() << "applySchema: Schema file does not exist:" << schemaPath;
+        return;
+    }
     QFile schemaFile(schemaPath);
     if (!schemaFile.open(QIODevice::ReadOnly)) {
-        qCritical() << "Could not find or open schema file:" << schemaPath;
+        qCritical() << "Could not open schema file:" << schemaPath;
         return;
     }
 
@@ -180,8 +188,12 @@ void DatabaseManager::applySchema(QSqlQuery& query, const QString& schemaPath) {
         }
     }
     // Ensure version and ID are set
-    query.exec(QString("PRAGMA user_version = %1").arg(USER_SCHEMA_VERSION));
-    query.exec(QString("PRAGMA application_id = %1").arg(APP_ID_USER));
+    if (!query.exec(QString("PRAGMA user_version = %1").arg(USER_SCHEMA_VERSION))) {
+        qCritical() << "Failed to set user_version:" << query.lastError().text();
+    }
+    if (!query.exec(QString("PRAGMA application_id = %1").arg(APP_ID_USER))) {
+        qCritical() << "Failed to set application_id:" << query.lastError().text();
+    }
     qDebug() << "Upgraded user database version to" << USER_SCHEMA_VERSION << "and set App ID.";
 
     // --- Seeding Data ---
diff --git a/src/db/reciperepository.cpp b/src/db/reciperepository.cpp
new file mode 100644 (file)
index 0000000..09d51a0
--- /dev/null
@@ -0,0 +1,177 @@
+#include "db/reciperepository.h"
+
+#include <QDebug>
+#include <QSqlDatabase>
+#include <QSqlError>
+#include <QSqlQuery>
+#include <QVariant>
+
+#include "db/databasemanager.h"
+
+RecipeRepository::RecipeRepository() {}
+
+int RecipeRepository::createRecipe(const QString& name, const QString& instructions) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return -1;
+
+    QSqlQuery query(db);
+    query.prepare("INSERT INTO recipe (name, instructions) VALUES (?, ?)");
+    query.addBindValue(name);
+    query.addBindValue(instructions);
+
+    if (query.exec()) {
+        return query.lastInsertId().toInt();
+    } else {
+        qCritical() << "Failed to create recipe:" << query.lastError().text();
+        return -1;
+    }
+}
+
+bool RecipeRepository::updateRecipe(int id, const QString& name, const QString& instructions) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return false;
+
+    QSqlQuery query(db);
+    query.prepare("UPDATE recipe SET name = ?, instructions = ? WHERE id = ?");
+    query.addBindValue(name);
+    query.addBindValue(instructions);
+    query.addBindValue(id);
+
+    return query.exec();
+}
+
+bool RecipeRepository::deleteRecipe(int id) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return false;
+
+    QSqlQuery query(db);
+    query.prepare("DELETE FROM recipe WHERE id = ?");
+    query.addBindValue(id);
+    return query.exec();
+}
+
+std::vector<RecipeItem> RecipeRepository::getAllRecipes() {
+    std::vector<RecipeItem> recipes;
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return recipes;
+
+    QSqlQuery query(db);
+    // TODO: Join with ingredient amounts * food nutrient values to get calories?
+    // For now, simple list.
+    if (query.exec("SELECT id, uuid, name, instructions, created FROM recipe ORDER BY name ASC")) {
+        while (query.next()) {
+            RecipeItem item;
+            item.id = query.value(0).toInt();
+            item.uuid = query.value(1).toString();
+            item.name = query.value(2).toString();
+            item.instructions = query.value(3).toString();
+            item.created = QDateTime::fromSecsSinceEpoch(query.value(4).toLongLong());
+            recipes.push_back(item);
+        }
+    } else {
+        qCritical() << "Failed to fetch recipes:" << query.lastError().text();
+    }
+    return recipes;
+}
+
+RecipeItem RecipeRepository::getRecipe(int id) {
+    RecipeItem item;
+    item.id = -1;
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return item;
+
+    QSqlQuery query(db);
+    query.prepare("SELECT id, uuid, name, instructions, created FROM recipe WHERE id = ?");
+    query.addBindValue(id);
+    if (query.exec() && query.next()) {
+        item.id = query.value(0).toInt();
+        item.uuid = query.value(1).toString();
+        item.name = query.value(2).toString();
+        item.instructions = query.value(3).toString();
+        item.created = QDateTime::fromSecsSinceEpoch(query.value(4).toLongLong());
+    }
+    return item;
+}
+
+bool RecipeRepository::addIngredient(int recipeId, int foodId, double amount) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return false;
+
+    QSqlQuery query(db);
+    query.prepare("INSERT INTO recipe_ingredient (recipe_id, food_id, amount) VALUES (?, ?, ?)");
+    query.addBindValue(recipeId);
+    query.addBindValue(foodId);
+    query.addBindValue(amount);
+
+    if (!query.exec()) {
+        qCritical() << "Failed to add ingredient:" << query.lastError().text();
+        return false;
+    }
+    return true;
+}
+
+bool RecipeRepository::removeIngredient(int recipeId, int foodId) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return false;
+
+    QSqlQuery query(db);
+    query.prepare("DELETE FROM recipe_ingredient WHERE recipe_id = ? AND food_id = ?");
+    query.addBindValue(recipeId);
+    query.addBindValue(foodId);
+    return query.exec();
+}
+
+bool RecipeRepository::updateIngredient(int recipeId, int foodId, double amount) {
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return false;
+
+    QSqlQuery query(db);
+    query.prepare("UPDATE recipe_ingredient SET amount = ? WHERE recipe_id = ? AND food_id = ?");
+    query.addBindValue(amount);
+    query.addBindValue(recipeId);
+    query.addBindValue(foodId);
+    return query.exec();
+}
+
+std::vector<RecipeIngredient> RecipeRepository::getIngredients(int recipeId) {
+    std::vector<RecipeIngredient> ingredients;
+    QSqlDatabase db = DatabaseManager::instance().userDatabase();
+    if (!db.isOpen()) return ingredients;
+
+    // We need to join with USDA db 'food_des' to get names?
+    // USDA db is attached as what? 'main' is User db usually?
+    // Wait, DatabaseManager opens main USDA db as 'db' (default connection) and User DB as
+    // 'user_db' (named connection). They are SEPARATE connections. Cross-database joins require
+    // attaching. DatabaseManager doesn't seem to attach them by default. workaround: Get IDs then
+    // query USDA db for names.
+
+    QSqlQuery query(db);
+    query.prepare("SELECT food_id, amount FROM recipe_ingredient WHERE recipe_id = ?");
+    query.addBindValue(recipeId);
+
+    if (query.exec()) {
+        while (query.next()) {
+            RecipeIngredient ing;
+            ing.foodId = query.value(0).toInt();
+            ing.amount = query.value(1).toDouble();
+
+            // Fetch name from main DB
+            // This is inefficient (N+1 queries), but simple for now without ATTACH logic.
+            // Or we could pass a list of IDs to FoodRepository.
+
+            QSqlDatabase usdaDb = DatabaseManager::instance().database();
+            if (usdaDb.isOpen()) {
+                QSqlQuery nameQuery(usdaDb);
+                nameQuery.prepare("SELECT long_desc FROM food_des WHERE ndb_no = ?");
+                nameQuery.addBindValue(ing.foodId);
+                if (nameQuery.exec() && nameQuery.next()) {
+                    ing.foodName = nameQuery.value(0).toString();
+                } else {
+                    ing.foodName = "Unknown Food (" + QString::number(ing.foodId) + ")";
+                }
+            }
+            ingredients.push_back(ing);
+        }
+    }
+    return ingredients;
+}
index 11f471c1db9b08a46aec0d44f999f1d64fd58245..3171c0752f8a1379a7cbcfe3303dc273440c13b2 100644 (file)
@@ -18,8 +18,11 @@ int main(int argc, char* argv[]) {
     QApplication::setWindowIcon(QIcon(":/resources/nutrition_icon-no_bg.png"));
 
     // Prevent multiple instances
-    QString lockPath =
-        QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/nutra.lock";
+    QString lockPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
+    if (lockPath.isEmpty()) {
+        lockPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
+    }
+    lockPath += "/nutra.lock";
     QLockFile lockFile(lockPath);
     if (!lockFile.tryLock(100)) {
         QMessageBox::warning(nullptr, "Nutra is already running",
index d47551ddcb7d52c0ed824e8b73f2f9242457de2e..d81f6d7342c408ff2844ad3bb19d8b077b18f1b8 100644 (file)
@@ -15,6 +15,7 @@
 #include "db/databasemanager.h"
 #include "widgets/preferencesdialog.h"
 #include "widgets/rdasettingswidget.h"
+#include "widgets/recipewidget.h"
 
 MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
     for (auto& recentFileAction : recentFileActions) {
@@ -116,6 +117,10 @@ void MainWindow::setupUi() {
     mealWidget = new MealWidget(this);
     tabs->addTab(mealWidget, "Meal Builder");
 
+    // Recipes Tab
+    recipeWidget = new RecipeWidget(this);
+    tabs->addTab(recipeWidget, "Recipes");
+
     // Daily Log Tab
     dailyLogWidget = new DailyLogWidget(this);
     tabs->addTab(dailyLogWidget, "Daily Log");
index 549305585e89e785203bcf7cca03f0c92663b10c..a070f4f680fc783f9b84f086c68909af42fd0e0c 100644 (file)
@@ -58,6 +58,7 @@ void ProfileSettingsWidget::setupUi() {
     // Activity Level
     activitySlider = new QSlider(Qt::Horizontal, this);
     activitySlider->setRange(1, 5);
+    activitySlider->setValue(2);  // Default to Lightly Active
     activitySlider->setTickPosition(QSlider::TicksBelow);
     activitySlider->setTickInterval(1);
 
diff --git a/src/widgets/recipewidget.cpp b/src/widgets/recipewidget.cpp
new file mode 100644 (file)
index 0000000..6784edf
--- /dev/null
@@ -0,0 +1,285 @@
+#include "widgets/recipewidget.h"
+
+#include <QDebug>
+#include <QHBoxLayout>
+#include <QHeaderView>
+#include <QInputDialog>
+#include <QMessageBox>
+#include <QSplitter>
+#include <QVBoxLayout>
+
+// Simple dialog to pick a food (MVP specific for ingredients)
+// Ideally we reuse SearchWidget, but wrapping it might be cleaner later.
+// For now, let's use a simple input dialog for ID, or we can make a tiny search dialog.
+#include <QDialog>
+
+#include "widgets/searchwidget.h"
+
+class IngredientSearchDialog : public QDialog {
+public:
+    IngredientSearchDialog(QWidget* parent) : QDialog(parent) {
+        setWindowTitle("Add Ingredient");
+        resize(600, 400);
+        auto* layout = new QVBoxLayout(this);
+        searchWidget = new SearchWidget(this);
+        layout->addWidget(searchWidget);
+
+        connect(searchWidget, &SearchWidget::foodSelected, this,
+                [this](int id, const QString& name) {
+                    selectedFoodId = id;
+                    selectedFoodName = name;
+                    accept();
+                });
+    }
+
+    int selectedFoodId = -1;
+    QString selectedFoodName;
+    SearchWidget* searchWidget;
+};
+
+RecipeWidget::RecipeWidget(QWidget* parent) : QWidget(parent) {
+    setupUi();
+    loadRecipes();
+}
+
+void RecipeWidget::setupUi() {
+    auto* mainLayout = new QHBoxLayout(this);
+
+    auto* splitter = new QSplitter(Qt::Horizontal, this);
+    mainLayout->addWidget(splitter);
+
+    // Left Pane: Recipe List
+    auto* leftWidget = new QWidget();
+    auto* leftLayout = new QVBoxLayout(leftWidget);
+    leftLayout->setContentsMargins(0, 0, 0, 0);
+
+    leftLayout->addWidget(new QLabel("Recipes", this));
+    recipeList = new QListWidget(this);
+    leftLayout->addWidget(recipeList);
+
+    newButton = new QPushButton("New Recipe", this);
+    leftLayout->addWidget(newButton);
+
+    leftWidget->setLayout(leftLayout);
+    splitter->addWidget(leftWidget);
+
+    // Right Pane: Details
+    auto* rightWidget = new QWidget();
+    auto* rightLayout = new QVBoxLayout(rightWidget);
+    rightLayout->setContentsMargins(0, 0, 0, 0);
+
+    // Name
+    auto* nameLayout = new QHBoxLayout();
+    nameLayout->addWidget(new QLabel("Name:", this));
+    nameEdit = new QLineEdit(this);
+    nameLayout->addWidget(nameEdit);
+    rightLayout->addLayout(nameLayout);
+
+    // Ingredients
+    rightLayout->addWidget(new QLabel("Ingredients:", this));
+    ingredientsTable = new QTableWidget(this);
+    ingredientsTable->setColumnCount(3);
+    ingredientsTable->setHorizontalHeaderLabels({"ID", "Food", "Amount (g)"});
+    ingredientsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
+    ingredientsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+    rightLayout->addWidget(ingredientsTable);
+
+    auto* ingButtonsLayout = new QHBoxLayout();
+    addIngredientButton = new QPushButton("Add Ingredient", this);
+    removeIngredientButton = new QPushButton("Remove Selected", this);
+    ingButtonsLayout->addWidget(addIngredientButton);
+    ingButtonsLayout->addWidget(removeIngredientButton);
+    ingButtonsLayout->addStretch();
+    rightLayout->addLayout(ingButtonsLayout);
+
+    // Instructions
+    rightLayout->addWidget(new QLabel("Instructions:", this));
+    instructionsEdit = new QTextEdit(this);
+    rightLayout->addWidget(instructionsEdit);
+
+    // Action Buttons
+    auto* actionLayout = new QHBoxLayout();
+    saveButton = new QPushButton("Save", this);
+    deleteButton = new QPushButton("Delete", this);
+    // deleteButton->setStyleSheet("background-color: #e74c3c; color: white;"); // User prefers
+    // styled?
+
+    actionLayout->addStretch();
+    actionLayout->addWidget(deleteButton);
+    actionLayout->addWidget(saveButton);
+    rightLayout->addLayout(actionLayout);
+
+    rightWidget->setLayout(rightLayout);
+    splitter->addWidget(rightWidget);
+
+    splitter->setStretchFactor(1, 2);  // Give details more space
+
+    // Connections
+    connect(newButton, &QPushButton::clicked, this, &RecipeWidget::onNewRecipe);
+    connect(recipeList, &QListWidget::itemSelectionChanged, this,
+            &RecipeWidget::onRecipeListSelectionChanged);
+    connect(saveButton, &QPushButton::clicked, this, &RecipeWidget::onSaveRecipe);
+    connect(deleteButton, &QPushButton::clicked, this, &RecipeWidget::onDeleteRecipe);
+    connect(addIngredientButton, &QPushButton::clicked, this, &RecipeWidget::onAddIngredient);
+    connect(removeIngredientButton, &QPushButton::clicked, this, &RecipeWidget::onRemoveIngredient);
+
+    clearDetails();
+}
+
+void RecipeWidget::loadRecipes() {
+    recipeList->clear();
+    auto recipes = repository.getAllRecipes();
+    for (const auto& r : recipes) {
+        auto* item = new QListWidgetItem(r.name, recipeList);
+        item->setData(Qt::UserRole, r.id);
+    }
+}
+
+void RecipeWidget::onRecipeListSelectionChanged() {
+    auto items = recipeList->selectedItems();
+    if (items.isEmpty()) {
+        clearDetails();
+        return;
+    }
+    int id = items.first()->data(Qt::UserRole).toInt();
+    loadRecipeDetails(id);
+}
+
+void RecipeWidget::loadRecipeDetails(int recipeId) {
+    currentRecipeId = recipeId;
+    RecipeItem item = repository.getRecipe(recipeId);
+    if (item.id == -1) return;  // Error
+
+    nameEdit->setText(item.name);
+    instructionsEdit->setText(item.instructions);
+
+    // Load ingredients
+    ingredientsTable->setRowCount(0);
+    auto ingredients = repository.getIngredients(recipeId);
+    ingredientsTable->setRowCount(ingredients.size());
+    for (int i = 0; i < ingredients.size(); ++i) {
+        const auto& ing = ingredients[i];
+        ingredientsTable->setItem(i, 0, new QTableWidgetItem(QString::number(ing.foodId)));
+        ingredientsTable->setItem(i, 1, new QTableWidgetItem(ing.foodName));
+        ingredientsTable->setItem(i, 2, new QTableWidgetItem(QString::number(ing.amount)));
+    }
+
+    saveButton->setEnabled(true);
+    deleteButton->setEnabled(true);
+}
+
+void RecipeWidget::clearDetails() {
+    currentRecipeId = -1;
+    nameEdit->clear();
+    instructionsEdit->clear();
+    ingredientsTable->setRowCount(0);
+    saveButton->setEnabled(false);
+    deleteButton->setEnabled(false);
+}
+
+void RecipeWidget::onNewRecipe() {
+    recipeList->clearSelection();
+    clearDetails();
+    nameEdit->setFocus();
+    saveButton->setEnabled(true);  // Allow saving a new one
+}
+
+void RecipeWidget::onSaveRecipe() {
+    QString name = nameEdit->text().trimmed();
+    if (name.isEmpty()) {
+        QMessageBox::warning(this, "Validation Error", "Recipe name cannot be empty.");
+        return;
+    }
+    QString instructions = instructionsEdit->toPlainText();
+
+    if (currentRecipeId == -1) {
+        // Create
+        int newId = repository.createRecipe(name, instructions);
+        if (newId != -1) {
+            currentRecipeId = newId;
+            // Add ingredients from table if any (though usually table is empty on new)
+            // But if user added ingredients before saving, we should handle that.
+            // Currently, adding ingredient requires a recipe ID?
+            // If strict: enforce save before adding ingredients.
+            // Or: keep ingredients in memory until save.
+            // For MVP: Simplest is: Create Recipe -> Then Add Ingredients.
+            // So if new, save creates empty recipe, then reloads it, allowing adds.
+            loadRecipes();
+            // Select the new item
+            for (int i = 0; i < recipeList->count(); ++i) {
+                if (recipeList->item(i)->data(Qt::UserRole).toInt() == newId) {
+                    recipeList->setCurrentRow(i);
+                    break;
+                }
+            }
+        }
+    } else {
+        // Update
+        repository.updateRecipe(currentRecipeId, name, instructions);
+
+        // Update ingredients?
+        // Ingredients are updated immediately in onAdd/RemoveIngredient for now?
+        // Or we should do batch save.
+        // Current implementation of onAddIngredient writes to DB immediately.
+        // So here just update name/instructions.
+
+        // Refresh list name if changed
+        auto items = recipeList->findItems(name, Qt::MatchExactly);  // This might find others?
+        // Just reload list to be safe or update current item
+        if (!recipeList->selectedItems().isEmpty()) {
+            recipeList->selectedItems().first()->setText(name);
+        }
+    }
+}
+
+void RecipeWidget::onDeleteRecipe() {
+    if (currentRecipeId == -1) return;
+
+    auto reply = QMessageBox::question(this, "Confirm Delete",
+                                       "Are you sure you want to delete this recipe?",
+                                       QMessageBox::Yes | QMessageBox::No);
+    if (reply == QMessageBox::Yes) {
+        repository.deleteRecipe(currentRecipeId);
+        loadRecipes();
+        clearDetails();
+    }
+}
+
+void RecipeWidget::onAddIngredient() {
+    if (currentRecipeId == -1) {
+        QMessageBox::information(this, "Save Required",
+                                 "Please save the recipe before adding ingredients.");
+        return;
+    }
+
+    IngredientSearchDialog dlg(this);
+    if (dlg.exec() == QDialog::Accepted) {
+        int foodId = dlg.selectedFoodId;
+        QString foodName = dlg.selectedFoodName;
+
+        // Ask for amount
+        bool ok;
+        double amount = QInputDialog::getDouble(
+            this, "Ingredient Amount", QString("Enter amount (grams) for %1:").arg(foodName), 100.0,
+            0.1, 10000.0, 1, &ok);
+
+        if (ok) {
+            if (repository.addIngredient(currentRecipeId, foodId, amount)) {
+                loadRecipeDetails(currentRecipeId);  // Refresh table
+            }
+        }
+    }
+}
+
+void RecipeWidget::onRemoveIngredient() {
+    if (currentRecipeId == -1) return;
+
+    int row = ingredientsTable->currentRow();
+    if (row < 0) return;
+
+    int foodId = ingredientsTable->item(row, 0)->text().toInt();
+
+    if (repository.removeIngredient(currentRecipeId, foodId)) {
+        ingredientsTable->removeRow(row);
+    }
+}
index 2f2cce8d4869503c84ccec1945c3e9f20afde589..738f3945056e07b7a25f0a79ad9a264a8dee3048 100644 (file)
@@ -244,7 +244,9 @@ void SearchWidget::updateCompleterModel() {
 }
 
 void SearchWidget::onCompleterActivated(const QString& text) {
+    searchInput->blockSignals(true);
     searchInput->setText(text);
+    searchInput->blockSignals(false);
     performSearch();
 }
 
index 52450bf173091ebd7ce69b6d4905ba506531b34e..583d5dfbf6ad7b80ae4fff30e2d0ba21de0fd567 100644 (file)
@@ -1,20 +1,14 @@
-#include <QtTest>
+#include "test_calculations.h"
 
-class TestCalculations : public QObject {
-    Q_OBJECT
+void TestCalculations::testBMR() {
+    // TDD: Fail mainly because not implemented
+    QEXPECT_FAIL("", "BMR calculation not yet implemented", Continue);
+    QVERIFY(false);
+}
 
-private slots:
-    void testBMR() {
-        // TDD: Fail mainly because not implemented
-        QEXPECT_FAIL("", "BMR calculation not yet implemented", Continue);
-        QVERIFY(false);
-    }
-
-    void testBodyFat() {
-        QEXPECT_FAIL("", "Body Fat calculation not yet implemented", Continue);
-        QVERIFY(false);
-    }
-};
+void TestCalculations::testBodyFat() {
+    QEXPECT_FAIL("", "Body Fat calculation not yet implemented", Continue);
+    QVERIFY(false);
+}
 
 QTEST_MAIN(TestCalculations)
-#include "test_calculations.moc"
diff --git a/tests/test_calculations.h b/tests/test_calculations.h
new file mode 100644 (file)
index 0000000..79edf79
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef TEST_CALCULATIONS_H
+#define TEST_CALCULATIONS_H
+
+#include <QObject>
+#include <QtTest>
+
+class TestCalculations : public QObject {
+    Q_OBJECT
+
+private slots:
+    void testBMR();
+    void testBodyFat();
+};
+
+#endif  // TEST_CALCULATIONS_H
index 65e88e5447ce148553cafdd18f972c76a4d7f3e4..a4b7538f27890dcc9f56b172e28312d632e542c3 100644 (file)
@@ -1,68 +1,55 @@
+#include "test_databasemanager.h"
+
 #include <QDir>
 #include <QFileInfo>
 #include <QSqlError>
 #include <QSqlQuery>
-#include <QtTest>
 
 #include "db/databasemanager.h"
 
-class TestDatabaseManager : public QObject {
-    Q_OBJECT
-
-private slots:
-    void testUserDatabaseInit() {
-        // Use a temporary database path
-        QString dbPath = QDir::tempPath() + "/nutra_test_db.sqlite3";
-        if (QFileInfo::exists(dbPath)) {
-            QFile::remove(dbPath);
-        }
-
-        // We can't easily instruct DatabaseManager to use a specific path for userDatabase()
-        // without modifying it to accept a path injection or using a mock.
-        // However, `DatabaseManager::connect` allows opening arbitrary databases.
-
-        // Let's test the validity check on a fresh DB.
-
-        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "test_connection");
-        db.setDatabaseName(dbPath);
-        QVERIFY(db.open());
+void TestDatabaseManager::testUserDatabaseInit() {
+    // Use a temporary database path
+    QString dbPath = QDir::tempPath() + "/nutra_test_db.sqlite3";
+    if (QFileInfo::exists(dbPath)) {
+        QFile::remove(dbPath);
+    }
 
-        // Initialize schema manually (simulating initUserDatabase behavior if we can't invoke it
-        // directly) OR, verify the one in ~/.nutra if we want integration test. Let's assume we
-        // want to verify the logic in DatabaseManager::getDatabaseInfo which requires a db on disk.
+    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "test_connection");
+    db.setDatabaseName(dbPath);
+    QVERIFY(db.open());
 
-        // Let's create a minimal valid user DB
-        QSqlQuery q(db);
-        q.exec("PRAGMA application_id = 1314145346");  // 'NTDB'
-        q.exec("PRAGMA user_version = 9");
-        q.exec("CREATE TABLE log_food (id int)");
+    // Initialize schema manually (simulating initUserDatabase behavior)
+    QSqlQuery q(db);
+    q.exec("PRAGMA application_id = 1314145346");  // 'NTDB'
+    q.exec("PRAGMA user_version = 9");
+    q.exec("CREATE TABLE log_food (id int)");
 
-        db.close();
+    db.close();
 
-        auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
-        QCOMPARE(info.type, QString("User"));
-        QVERIFY(info.isValid);
-        QCOMPARE(info.version, 9);
+    auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
+    QCOMPARE(info.type, QString("User"));
+    QVERIFY(info.isValid);
+    QCOMPARE(info.version, 9);
 
-        QFile::remove(dbPath);
-    }
+    QSqlDatabase::removeDatabase("test_connection");
+    QFile::remove(dbPath);
+}
 
-    void testInvalidDatabase() {
-        QString dbPath = QDir::tempPath() + "/nutra_invalid.sqlite3";
-        if (QFileInfo::exists(dbPath)) QFile::remove(dbPath);
+void TestDatabaseManager::testInvalidDatabase() {
+    QString dbPath = QDir::tempPath() + "/nutra_invalid.sqlite3";
+    if (QFileInfo::exists(dbPath)) QFile::remove(dbPath);
 
-        QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "invalid_conn");
-        db.setDatabaseName(dbPath);
-        QVERIFY(db.open());
-        // Empty DB
-        db.close();
+    QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE", "invalid_conn");
+    db.setDatabaseName(dbPath);
+    QVERIFY(db.open());
+    // Empty DB
+    db.close();
 
-        auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
-        QVERIFY(info.isValid == false);
+    auto info = DatabaseManager::instance().getDatabaseInfo(dbPath);
+    QVERIFY(info.isValid == false);
 
-        QFile::remove(dbPath);
-    }
-};
+    QSqlDatabase::removeDatabase("invalid_conn");
+    QFile::remove(dbPath);
+}
 
 QTEST_MAIN(TestDatabaseManager)
-#include "test_databasemanager.moc"
diff --git a/tests/test_databasemanager.h b/tests/test_databasemanager.h
new file mode 100644 (file)
index 0000000..3c52af9
--- /dev/null
@@ -0,0 +1,15 @@
+#ifndef TEST_DATABASEMANAGER_H
+#define TEST_DATABASEMANAGER_H
+
+#include <QObject>
+#include <QtTest>
+
+class TestDatabaseManager : public QObject {
+    Q_OBJECT
+
+private slots:
+    void testUserDatabaseInit();
+    void testInvalidDatabase();
+};
+
+#endif  // TEST_DATABASEMANAGER_H