]> Nutra Git (v2) - nutratech/gui.git/commitdiff
user db and some ui improvements
authorShane Jaroch <chown_tee@proton.me>
Wed, 21 Jan 2026 11:59:34 +0000 (06:59 -0500)
committerShane Jaroch <chown_tee@proton.me>
Wed, 21 Jan 2026 11:59:34 +0000 (06:59 -0500)
12 files changed:
CMakeLists.txt
include/db/databasemanager.h
include/db/foodrepository.h
include/mainwindow.h
include/widgets/rdasettingswidget.h [new file with mode: 0644]
include/widgets/weightinputdialog.h [new file with mode: 0644]
src/db/databasemanager.cpp
src/db/foodrepository.cpp
src/mainwindow.cpp
src/widgets/rdasettingswidget.cpp [new file with mode: 0644]
src/widgets/searchwidget.cpp
src/widgets/weightinputdialog.cpp [new file with mode: 0644]

index a638c40228b9af2468d3a90ecfece69a43169893..e5d7ea78d33bab6cf6f072153b949e1be6e8cdcb 100644 (file)
@@ -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
index 32ad4eabcc6d155d51b19761c433f10a100c26ae..f6597e60a636c23a16dd6fa574f648138d5d5680 100644 (file)
@@ -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
index f90fc2b5184a79a546e6dd33d9ce9985721bf6e1..e2503e4ab4383ec2d3d059b31c315acc7e8ff0aa 100644 (file)
@@ -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<Nutrient> getFoodNutrients(int foodId);
 
+  // Get available serving weights (units) for a food
+  std::vector<ServingWeight> getFoodServings(int foodId);
+
+  // RDA methods
+  std::map<int, double> 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<FoodItem> m_cache;
+  std::map<int, double> m_rdas;
 };
 
 #endif // FOODREPOSITORY_H
index 12e4bf22e33d3a352c5a52241fdfa1a12a8ea56a..1a8c277b2bdfc69fb58c20ef969010425080836e 100644 (file)
@@ -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 (file)
index 0000000..c8284f0
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef RDASETTINGSWIDGET_H
+#define RDASETTINGSWIDGET_H
+
+#include "db/foodrepository.h"
+#include <QDialog>
+#include <QTableWidget>
+
+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 (file)
index 0000000..bbc13a4
--- /dev/null
@@ -0,0 +1,29 @@
+#ifndef WEIGHTINPUTDIALOG_H
+#define WEIGHTINPUTDIALOG_H
+
+#include "db/foodrepository.h"
+#include <QComboBox>
+#include <QDialog>
+#include <QDoubleSpinBox>
+#include <QLabel>
+
+class WeightInputDialog : public QDialog {
+  Q_OBJECT
+
+public:
+  explicit WeightInputDialog(const QString &foodName,
+                             const std::vector<ServingWeight> &servings,
+                             QWidget *parent = nullptr);
+
+  double getGrams() const;
+
+private:
+  QDoubleSpinBox *amountSpinBox;
+  QComboBox *unitComboBox;
+  std::vector<ServingWeight> m_servings;
+
+  static constexpr double GRAMS_PER_OZ = 28.3495;
+  static constexpr double GRAMS_PER_LB = 453.592;
+};
+
+#endif // WEIGHTINPUTDIALOG_H
index abda07d54a917be8fa92bbbd633db68683d7022a..131b87075759a83677b5f1c116e42ee3ba353cb9 100644 (file)
@@ -1,14 +1,19 @@
 #include "db/databasemanager.h"
 #include <QDebug>
+#include <QDir>
 #include <QFileInfo>
 #include <QSqlError>
+#include <QVariant>
 
 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();
+  }
+}
index 3d184174da382f5ad44b9a3a1a48b6076076a469..a18689539df54c4869568942075904e1303af74b 100644 (file)
@@ -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<int, int> 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<FoodItem> FoodRepository::searchFoods(const QString &query) {
   ensureCacheLoaded();
   std::vector<FoodItem> results;
@@ -121,7 +148,12 @@ std::vector<FoodItem> 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<Nutrient> 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<Nutrient> FoodRepository::getFoodNutrients(int foodId) {
 
   return results;
 }
+
+std::vector<ServingWeight> FoodRepository::getFoodServings(int foodId) {
+  std::vector<ServingWeight> 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<int, double> 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();
+  }
+}
index 869fab5350085803958d4aaa21a9506bbca8c934..9c6d2e6ff395fae9bd2355b2b80e6817645c19ba 100644 (file)
@@ -1,5 +1,6 @@
 #include "mainwindow.h"
 #include "db/databasemanager.h"
+#include "widgets/rdasettingswidget.h"
 #include <QAction>
 #include <QDebug>
 #include <QFileDialog>
@@ -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 (file)
index 0000000..877fff3
--- /dev/null
@@ -0,0 +1,84 @@
+#include "widgets/rdasettingswidget.h"
+#include "db/databasemanager.h"
+#include <QHeaderView>
+#include <QLabel>
+#include <QSqlQuery>
+#include <QVBoxLayout>
+
+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);
+}
index 29f7aa5c9608bf3498c16e86bf295eff1ab83ce8..37193c66a78ae5c57c89537fedc390761999872c 100644 (file)
@@ -1,8 +1,8 @@
 #include "widgets/searchwidget.h"
+#include "widgets/weightinputdialog.h"
 #include <QAction>
 #include <QHBoxLayout>
 #include <QHeaderView>
-#include <QInputDialog>
 #include <QMenu>
 #include <QMessageBox>
 #include <QVBoxLayout>
@@ -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<ServingWeight> 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 (file)
index 0000000..b5e2b5d
--- /dev/null
@@ -0,0 +1,63 @@
+#include "widgets/weightinputdialog.h"
+#include <QHBoxLayout>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+WeightInputDialog::WeightInputDialog(const QString &foodName,
+                                     const std::vector<ServingWeight> &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;
+}