From: Shane Jaroch Date: Wed, 21 Jan 2026 11:59:34 +0000 (-0500) Subject: user db and some ui improvements X-Git-Url: https://git.nutra.tk/v1?a=commitdiff_plain;h=28debccafd067194dc1f1228fc9cc6ccb6571ad1;p=nutratech%2Fgui.git user db and some ui improvements --- diff --git a/CMakeLists.txt b/CMakeLists.txt index a638c40..e5d7ea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,23 +13,10 @@ set(CMAKE_AUTOUIC ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Sql) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Sql) -set(PROJECT_SOURCES - src/main.cpp - src/mainwindow.cpp - include/mainwindow.h - src/db/databasemanager.cpp - include/db/databasemanager.h - src/db/foodrepository.cpp - include/db/foodrepository.h - src/widgets/searchwidget.cpp - include/widgets/searchwidget.h - src/widgets/detailswidget.cpp - include/widgets/detailswidget.h - src/widgets/mealwidget.cpp - include/widgets/mealwidget.h - src/utils/string_utils.cpp - include/utils/string_utils.h - resources.qrc +file(GLOB_RECURSE PROJECT_SOURCES + "src/*.cpp" + "include/*.h" + "resources.qrc" ) # Versioning diff --git a/include/db/databasemanager.h b/include/db/databasemanager.h index 32ad4ea..f6597e6 100644 --- a/include/db/databasemanager.h +++ b/include/db/databasemanager.h @@ -11,6 +11,7 @@ public: 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; @@ -19,7 +20,10 @@ private: DatabaseManager(); ~DatabaseManager(); + void initUserDatabase(); + QSqlDatabase m_db; + QSqlDatabase m_userDb; }; #endif // DATABASEMANAGER_H diff --git a/include/db/foodrepository.h b/include/db/foodrepository.h index f90fc2b..e2503e4 100644 --- a/include/db/foodrepository.h +++ b/include/db/foodrepository.h @@ -13,10 +13,15 @@ struct Nutrient { double rdaPercentage; // Calculated }; +struct ServingWeight { + QString description; + double grams; +}; + struct FoodItem { int id; QString description; - int foodGroupId; + QString foodGroupName; int nutrientCount; int aminoCount; int flavCount; @@ -35,16 +40,25 @@ public: // Returns a list of nutrients std::vector getFoodNutrients(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); + // Helper to get nutrient definition basics if needed // QString getNutrientName(int nutrientId); private: // Internal helper methods void ensureCacheLoaded(); + void loadRdas(); bool m_cacheLoaded = false; // Cache stores basic food info std::vector m_cache; + std::map m_rdas; }; #endif // FOODREPOSITORY_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 12e4bf2..1a8c277 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -29,6 +29,7 @@ private: SearchWidget *searchWidget; DetailsWidget *detailsWidget; MealWidget *mealWidget; + FoodRepository repository; QMenu *recentFilesMenu; static constexpr int MaxRecentFiles = 5; diff --git a/include/widgets/rdasettingswidget.h b/include/widgets/rdasettingswidget.h new file mode 100644 index 0000000..c8284f0 --- /dev/null +++ b/include/widgets/rdasettingswidget.h @@ -0,0 +1,26 @@ +#ifndef RDASETTINGSWIDGET_H +#define RDASETTINGSWIDGET_H + +#include "db/foodrepository.h" +#include +#include + +class RDASettingsWidget : public QDialog { + Q_OBJECT + +public: + explicit RDASettingsWidget(FoodRepository &repository, + QWidget *parent = nullptr); + +private slots: + void onCellChanged(int row, int column); + +private: + void loadData(); + + FoodRepository &m_repository; + QTableWidget *m_table; + bool m_loading = false; +}; + +#endif // RDASETTINGSWIDGET_H diff --git a/include/widgets/weightinputdialog.h b/include/widgets/weightinputdialog.h new file mode 100644 index 0000000..bbc13a4 --- /dev/null +++ b/include/widgets/weightinputdialog.h @@ -0,0 +1,29 @@ +#ifndef WEIGHTINPUTDIALOG_H +#define WEIGHTINPUTDIALOG_H + +#include "db/foodrepository.h" +#include +#include +#include +#include + +class WeightInputDialog : public QDialog { + Q_OBJECT + +public: + explicit WeightInputDialog(const QString &foodName, + const std::vector &servings, + QWidget *parent = nullptr); + + double getGrams() const; + +private: + QDoubleSpinBox *amountSpinBox; + QComboBox *unitComboBox; + std::vector m_servings; + + static constexpr double GRAMS_PER_OZ = 28.3495; + static constexpr double GRAMS_PER_LB = 453.592; +}; + +#endif // WEIGHTINPUTDIALOG_H diff --git a/src/db/databasemanager.cpp b/src/db/databasemanager.cpp index abda07d..131b870 100644 --- a/src/db/databasemanager.cpp +++ b/src/db/databasemanager.cpp @@ -1,14 +1,19 @@ #include "db/databasemanager.h" #include +#include #include #include +#include DatabaseManager &DatabaseManager::instance() { static DatabaseManager instance; return instance; } -DatabaseManager::DatabaseManager() = default; +DatabaseManager::DatabaseManager() { + m_userDb = QSqlDatabase::addDatabase("QSQLITE", "user_db"); + initUserDatabase(); +} DatabaseManager::~DatabaseManager() { if (m_db.isOpen()) { @@ -41,3 +46,38 @@ bool DatabaseManager::connect(const QString &path) { bool DatabaseManager::isOpen() const { return m_db.isOpen(); } QSqlDatabase DatabaseManager::database() const { return m_db; } + +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(); + } +} diff --git a/src/db/foodrepository.cpp b/src/db/foodrepository.cpp index 3d18417..a186895 100644 --- a/src/db/foodrepository.cpp +++ b/src/db/foodrepository.cpp @@ -21,8 +21,11 @@ void FoodRepository::ensureCacheLoaded() { if (!db.isOpen()) return; - // 1. Load Food Items - QSqlQuery query("SELECT id, long_desc, fdgrp_id FROM food_des", db); + // 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) @@ -36,7 +39,7 @@ void FoodRepository::ensureCacheLoaded() { FoodItem item; item.id = query.value(0).toInt(); item.description = query.value(1).toString(); - item.foodGroupId = query.value(2).toInt(); + item.foodGroupName = query.value(2).toString(); // Set counts from map (default 0 if not found) auto it = nutrientCounts.find(item.id); @@ -47,9 +50,33 @@ void FoodRepository::ensureCacheLoaded() { 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(); + + // 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(); + } + } + + // 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(); + } + } +} + std::vector FoodRepository::searchFoods(const QString &query) { ensureCacheLoaded(); std::vector results; @@ -121,7 +148,12 @@ std::vector FoodRepository::searchFoods(const QString &query) { nut.amount = nutQuery.value(2).toDouble(); nut.description = nutQuery.value(3).toString(); nut.unit = nutQuery.value(4).toString(); - nut.rdaPercentage = 0.0; + + 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); @@ -164,7 +196,12 @@ std::vector FoodRepository::getFoodNutrients(int foodId) { nut.amount = query.value(1).toDouble(); nut.description = query.value(2).toString(); nut.unit = query.value(3).toString(); - nut.rdaPercentage = 0.0; + + 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); } @@ -175,3 +212,58 @@ std::vector FoodRepository::getFoodNutrients(int foodId) { return results; } + +std::vector FoodRepository::getFoodServings(int foodId) { + std::vector results; + QSqlDatabase db = DatabaseManager::instance().database(); + + if (!db.isOpen()) + return results; + + 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; + } + + 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(); + } + + return results; +} + +std::map FoodRepository::getNutrientRdas() { + 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(); + } +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 869fab5..9c6d2e6 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,5 +1,6 @@ #include "mainwindow.h" #include "db/databasemanager.h" +#include "widgets/rdasettingswidget.h" #include #include #include @@ -42,8 +43,14 @@ void MainWindow::setupUi() { recentFilesMenu->addAction(recentFileActions[i]); // Edit Menu - auto *editMenu = menuBar()->addMenu("&Edit"); - auto *settingsAction = editMenu->addAction("&Settings"); + 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 diff --git a/src/widgets/rdasettingswidget.cpp b/src/widgets/rdasettingswidget.cpp new file mode 100644 index 0000000..877fff3 --- /dev/null +++ b/src/widgets/rdasettingswidget.cpp @@ -0,0 +1,84 @@ +#include "widgets/rdasettingswidget.h" +#include "db/databasemanager.h" +#include +#include +#include +#include + +RDASettingsWidget::RDASettingsWidget(FoodRepository &repository, + QWidget *parent) + : QDialog(parent), m_repository(repository) { + 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.")); + + 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(); + + connect(m_table, &QTableWidget::cellChanged, this, + &RDASettingsWidget::onCellChanged); + + layout->addWidget(m_table); +} + +void RDASettingsWidget::loadData() { + m_loading = true; + m_table->setRowCount(0); + + 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(); + + 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); + + 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 *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); + + row++; + } + + m_loading = false; +} + +void RDASettingsWidget::onCellChanged(int row, int column) { + if (m_loading || column != 2) + return; + + int id = m_table->item(row, 0)->text().toInt(); + double value = m_table->item(row, 2)->text().toDouble(); + + m_repository.updateRda(id, value); +} diff --git a/src/widgets/searchwidget.cpp b/src/widgets/searchwidget.cpp index 29f7aa5..37193c6 100644 --- a/src/widgets/searchwidget.cpp +++ b/src/widgets/searchwidget.cpp @@ -1,8 +1,8 @@ #include "widgets/searchwidget.h" +#include "widgets/weightinputdialog.h" #include #include #include -#include #include #include #include @@ -68,8 +68,7 @@ void SearchWidget::performSearch() { 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(QString::number(item.foodGroupId))); + resultsTable->setItem(i, 2, new QTableWidgetItem(item.foodGroupName)); resultsTable->setItem( i, 3, new QTableWidgetItem(QString::number(item.nutrientCount))); resultsTable->setItem( @@ -116,11 +115,10 @@ void SearchWidget::onCustomContextMenu(const QPoint &pos) { if (selectedAction == analyzeAction) { emit foodSelected(foodId, foodName); } else if (selectedAction == addToMealAction) { - bool ok; - double grams = QInputDialog::getDouble( - this, "Add to Meal", "Amount (grams):", 100.0, 0.1, 10000.0, 1, &ok); - if (ok) { - emit addToMealRequested(foodId, foodName, grams); + 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 new file mode 100644 index 0000000..b5e2b5d --- /dev/null +++ b/src/widgets/weightinputdialog.cpp @@ -0,0 +1,63 @@ +#include "widgets/weightinputdialog.h" +#include +#include +#include + +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); + + 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; +}