From: Shane Jaroch Date: Thu, 22 Jan 2026 06:39:20 +0000 (-0500) Subject: update X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=338a2c5f3ae09b845e27eccfc310351f36a76233;p=nutratech%2Fgui.git update --- diff --git a/CMakeLists.txt b/CMakeLists.txt index f4c8dfb..07ea4d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 index 0000000..57914e5 --- /dev/null +++ b/include/db/reciperepository.h @@ -0,0 +1,43 @@ +#ifndef RECIPEREPOSITORY_H +#define RECIPEREPOSITORY_H + +#include +#include +#include + +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 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 getIngredients(int recipeId); +}; + +#endif // RECIPEREPOSITORY_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 68f1524..192679c 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -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 index 0000000..5ca58b4 --- /dev/null +++ b/include/widgets/recipewidget.h @@ -0,0 +1,55 @@ +#ifndef RECIPEWIDGET_H +#define RECIPEWIDGET_H + +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/lib/ntsqlite b/lib/ntsqlite index acd5af5..97934ad 160000 --- a/lib/ntsqlite +++ b/lib/ntsqlite @@ -1 +1 @@ -Subproject commit acd5af5d0d87f7683086788ebcba94197cb5b660 +Subproject commit 97934ada10143640d4a3aa326cf1c9c8f245c65a diff --git a/src/db/databasemanager.cpp b/src/db/databasemanager.cpp index adab275..a957a16 100644 --- a/src/db/databasemanager.cpp +++ b/src/db/databasemanager.cpp @@ -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 index 0000000..09d51a0 --- /dev/null +++ b/src/db/reciperepository.cpp @@ -0,0 +1,177 @@ +#include "db/reciperepository.h" + +#include +#include +#include +#include +#include + +#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 RecipeRepository::getAllRecipes() { + std::vector 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 RecipeRepository::getIngredients(int recipeId) { + std::vector 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; +} diff --git a/src/main.cpp b/src/main.cpp index 11f471c..3171c07 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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", diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d47551d..d81f6d7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -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"); diff --git a/src/widgets/profilesettingswidget.cpp b/src/widgets/profilesettingswidget.cpp index 5493055..a070f4f 100644 --- a/src/widgets/profilesettingswidget.cpp +++ b/src/widgets/profilesettingswidget.cpp @@ -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 index 0000000..6784edf --- /dev/null +++ b/src/widgets/recipewidget.cpp @@ -0,0 +1,285 @@ +#include "widgets/recipewidget.h" + +#include +#include +#include +#include +#include +#include +#include + +// 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 + +#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); + } +} diff --git a/src/widgets/searchwidget.cpp b/src/widgets/searchwidget.cpp index 2f2cce8..738f394 100644 --- a/src/widgets/searchwidget.cpp +++ b/src/widgets/searchwidget.cpp @@ -244,7 +244,9 @@ void SearchWidget::updateCompleterModel() { } void SearchWidget::onCompleterActivated(const QString& text) { + searchInput->blockSignals(true); searchInput->setText(text); + searchInput->blockSignals(false); performSearch(); } diff --git a/tests/test_calculations.cpp b/tests/test_calculations.cpp index 52450bf..583d5df 100644 --- a/tests/test_calculations.cpp +++ b/tests/test_calculations.cpp @@ -1,20 +1,14 @@ -#include +#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 index 0000000..79edf79 --- /dev/null +++ b/tests/test_calculations.h @@ -0,0 +1,15 @@ +#ifndef TEST_CALCULATIONS_H +#define TEST_CALCULATIONS_H + +#include +#include + +class TestCalculations : public QObject { + Q_OBJECT + +private slots: + void testBMR(); + void testBodyFat(); +}; + +#endif // TEST_CALCULATIONS_H diff --git a/tests/test_databasemanager.cpp b/tests/test_databasemanager.cpp index 65e88e5..a4b7538 100644 --- a/tests/test_databasemanager.cpp +++ b/tests/test_databasemanager.cpp @@ -1,68 +1,55 @@ +#include "test_databasemanager.h" + #include #include #include #include -#include #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 index 0000000..3c52af9 --- /dev/null +++ b/tests/test_databasemanager.h @@ -0,0 +1,15 @@ +#ifndef TEST_DATABASEMANAGER_H +#define TEST_DATABASEMANAGER_H + +#include +#include + +class TestDatabaseManager : public QObject { + Q_OBJECT + +private slots: + void testUserDatabaseInit(); + void testInvalidDatabase(); +}; + +#endif // TEST_DATABASEMANAGER_H