aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2021-04-08 08:49:10 -0700
committerrtk0c <[email protected]>2021-04-08 08:49:10 -0700
commitce8660cc5bfc12e6e3f75d4cce22492783ca9066 (patch)
treef5c1088def48e60570a1225d6440f304b37b5b5c /core
parent2f4b9db39239ed5150094a81743beea42a3eedc2 (diff)
Initial work on table visualizer
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt1
-rw-r--r--core/locale/zh_CN.json17
-rw-r--r--core/src/Model/Filter.cpp1
-rw-r--r--core/src/Model/Filter.hpp5
-rw-r--r--core/src/Model/Items.cpp9
-rw-r--r--core/src/Model/Items.hpp6
-rw-r--r--core/src/Model/Project.cpp8
-rw-r--r--core/src/Model/Project.hpp3
-rw-r--r--core/src/Model/TransactionDatabase.cpp161
-rw-r--r--core/src/Model/TransactionDatabase.hpp42
-rw-r--r--core/src/Model/fwd.hpp6
-rw-r--r--core/src/UI/Localization.hpp39
-rw-r--r--core/src/UI/UI_DatabaseView.cpp274
-rw-r--r--core/src/UI/UI_Items.cpp24
-rw-r--r--core/src/UI/UI_MainWindow.cpp13
-rw-r--r--core/src/Utils/Macros.hpp6
-rw-r--r--core/src/Utils/ScopeGuard.hpp35
17 files changed, 588 insertions, 62 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 0432ed1..02fa74a 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -34,6 +34,7 @@ set(ENTRYPOINT_MODULE_SOURCES
)
add_source_group(MODEL_MODULE_SOURCES
+ src/Model/Filter.cpp
src/Model/GlobalStates.cpp
src/Model/Items.cpp
src/Model/Project.cpp
diff --git a/core/locale/zh_CN.json b/core/locale/zh_CN.json
index 917d2f3..188ec4f 100644
--- a/core/locale/zh_CN.json
+++ b/core/locale/zh_CN.json
@@ -1,13 +1,15 @@
{
"$localized_name": "中文 - 中国",
"Generic.Error": "错误",
+ "Generic.Add": "\uf067 新建",
+ "Generic.Edit": "\uf044 编辑",
+ "Generic.Delete": "\uf1f8 删除",
"Generic.Dialog.Confirm": "确定",
"Generic.Dialog.Cancel": "取消",
"MainWindow.Tab.Settings": "\uf013 设置",
"MainWindow.Tab.Project": "\uf15b 项目",
"MainWindow.Tab.DatabaseView": "\uf1c0 数据",
"MainWindow.Tab.Items": "\uf466 物品",
- "MainWindow.Tab.Exports": "\uf56e 导出",
"Project.New": "新建项目...",
"Project.New.DialogTitle": "新建项目向导",
"Project.New.Name": "项目名称",
@@ -27,11 +29,16 @@
"ActiveProject.OpenInFilesystem": "\uf07b 在文件系统中打开",
"ActiveProject.Info.Name": "项目名称:",
"ActiveProject.Info.Path": "项目路径:",
- "Item.Add": "\uf067 新建",
+ "Database.SalesView.TabName": "销售",
+ "Database.SalesView.Edit.DialogTitle": "编辑销售记录",
+ "Database.PurchasesView.TabName": "采购",
+ "Database.DeliveriesView.TabName": "运输",
+ "Database.Column.Customer": "客户",
+ "Database.Column.Deadline": "交货期限",
+ "Database.Column.DeliveryTime": "交货时间",
+ "Database.Message.NotDelivered": "N/A",
"Item.Add.DialogTitle": "新建物品项",
- "Item.Edit": "\uf044 编辑",
"Item.Edit.DialogTitle": "编辑物品项",
- "Item.Delete": "\uf1f8 删除",
"Item.Delete.DialogTitle": "删除物品项",
"Item.Delete.DialogMessage": "确定删除该物品项吗?",
"Item.CategoryName.Product": "产品",
@@ -40,6 +47,8 @@
"Item.Column.Name": "名称",
"Item.Column.Description": "描述",
"Item.Column.Email": "邮箱",
+ "Item.Column.Stock": "库存",
+ "Item.Column.Price": "价格",
"Item.EmptyNameError": "产品名不能为空",
"Item.DuplicateNameError": "产品名已被占用",
} \ No newline at end of file
diff --git a/core/src/Model/Filter.cpp b/core/src/Model/Filter.cpp
new file mode 100644
index 0000000..1e4b31b
--- /dev/null
+++ b/core/src/Model/Filter.cpp
@@ -0,0 +1 @@
+#include "Filter.hpp"
diff --git a/core/src/Model/Filter.hpp b/core/src/Model/Filter.hpp
new file mode 100644
index 0000000..53995c1
--- /dev/null
+++ b/core/src/Model/Filter.hpp
@@ -0,0 +1,5 @@
+#pragma once
+
+class TableRowsFilter {
+ // TODO
+};
diff --git a/core/src/Model/Items.cpp b/core/src/Model/Items.cpp
index 2951666..02a4516 100644
--- a/core/src/Model/Items.cpp
+++ b/core/src/Model/Items.cpp
@@ -8,6 +8,13 @@ void ProductItem::SetDescription(std::string description) {
mDescription = std::move(description);
}
+int ProductItem::GetPrice() const {
+ return mPrice;
+}
+void ProductItem::SetPrice(int price) {
+ mPrice = price;
+}
+
int ProductItem::GetStock() const {
return mStock;
}
@@ -19,12 +26,14 @@ void ProductItem::SetStock(int stock) {
Json::Value ProductItem::Serialize() const {
Json::Value elm;
elm["Description"] = mDescription;
+ elm["Price"] = mPrice;
elm["Stock"] = mStock;
return elm;
}
void ProductItem::Deserialize(const Json::Value& elm) {
mDescription = elm["Description"].asString();
+ mPrice = elm["Price"].asInt();
mStock = elm["Stock"].asInt();
}
diff --git a/core/src/Model/Items.hpp b/core/src/Model/Items.hpp
index 14b62f3..1289be6 100644
--- a/core/src/Model/Items.hpp
+++ b/core/src/Model/Items.hpp
@@ -175,6 +175,7 @@ public:
class ProductItem : public ItemBase<ProductItem> {
private:
std::string mDescription;
+ int mPrice = 0;
int mStock = 0;
public:
@@ -182,6 +183,11 @@ public:
const std::string& GetDescription() const;
void SetDescription(std::string description);
+ /// Get the price of this item in US cents.
+ int GetPrice() const;
+ void SetPrice(int price);
+ /// Get the current number of this product in warehouse.
+ /// This is a housekeeping field and shouldn't be editable by the user from the UI.
int GetStock() const;
void SetStock(int stock);
diff --git a/core/src/Model/Project.cpp b/core/src/Model/Project.cpp
index c20e0c8..2a79e3f 100644
--- a/core/src/Model/Project.cpp
+++ b/core/src/Model/Project.cpp
@@ -82,6 +82,14 @@ void Project::SetName(std::string name) {
mName = std::move(name);
}
+const TransactionDatabase& Project::GetDatabase() const {
+ return mDb;
+}
+
+TransactionDatabase& Project::GetDatabase() {
+ return mDb;
+}
+
Json::Value Project::Serialize() {
Json::Value root(Json::objectValue);
diff --git a/core/src/Model/Project.hpp b/core/src/Model/Project.hpp
index 8d437ea..dca10d0 100644
--- a/core/src/Model/Project.hpp
+++ b/core/src/Model/Project.hpp
@@ -39,6 +39,9 @@ public:
const std::string& GetName() const;
void SetName(std::string name);
+ const TransactionDatabase& GetDatabase() const;
+ TransactionDatabase& GetDatabase();
+
Json::Value Serialize();
void WriteToDisk();
};
diff --git a/core/src/Model/TransactionDatabase.cpp b/core/src/Model/TransactionDatabase.cpp
index c28db0d..766727d 100644
--- a/core/src/Model/TransactionDatabase.cpp
+++ b/core/src/Model/TransactionDatabase.cpp
@@ -4,38 +4,157 @@
#include <filesystem>
#include <stdexcept>
-#include <string>
namespace fs = std::filesystem;
-static bool TableExists(sqlite3* db, const char* table, const char* column = nullptr) {
- return sqlite3_table_column_metadata(db, nullptr, table, column, nullptr, nullptr, nullptr, nullptr, nullptr) == SQLITE_OK;
+SalesTable::SalesTable(TransactionDatabase& db)
+ // language=SQLite
+ : GetRowsStatement(db.GetSQLite(), R"""(
+SELECT * FROM Sales WHERE rowid >= ? AND rowid < ?
+)""")
+ // language=SQLite
+ // TODO
+ , FilterRowsStatement(db.GetSQLite(), R"""(
+)""") {
+}
+
+int SalesTable::GetEntryCont() const {
+ // TODO
+ return 0;
+}
+
+DeliveryTable::DeliveryTable(TransactionDatabase& db) {
+}
+
+PurchasesTable::PurchasesTable(TransactionDatabase& db) {
+}
+
+static std::string GetDatabaseFilePath(const Project& project) {
+ auto dbsDir = project.GetPath() / "databases";
+ fs::create_directories(dbsDir);
+
+ auto dbFile = dbsDir / "transactions.sqlite3";
+ return dbFile.string();
}
TransactionDatabase::TransactionDatabase(Project& project)
: mProject{ &project }
- , mDatabase{ nullptr } {
+ , mDb(GetDatabaseFilePath(project), SQLite::OPEN_READWRITE)
+ , mSales(*this)
+ , mPurchases(*this)
+ , mDeliveries(*this) {
+ // Schema
+ // - Customer: the customer item ID
+ // - Deadline: unix epoch time of order deadline
+ // - DeliveryTime: the time this order was completed (through a set of deliveries)
+ if (!mDb.tableExists("Sales")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE Sales(
+ INT PRIMARY KEY,
+ Customer INT,
+ Deadline DATETIME,
+ DeliveryTime DATETIME
+);
+)""");
+ }
+
+ if (!mDb.tableExists("SalesItems")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE SalesItems(
+ SaleId INT,
+ ItemId INT,
+ Count INT
+);
+)""");
+ }
- fs::path dbDir = project.GetPath() / "databases";
- fs::create_directories(dbDir);
+ // Schema
+ // - Factory: the factory id,
+ // - OrderTime: the time this order was made
+ // - DeliveryTime: the time this order was completed (through a set of deliveries)
+ if (!mDb.tableExists("Purchases")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE Purchases(
+ INT PRIMARY KEY,
+ Factory INT,
+ OrderTime DATETIME,
+ DeliveryTime DATETIME
+);
+)""");
+ }
+
+ if (!mDb.tableExists("PurchasesItems")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE PurchasesItems(
+ PurchaseId INT,
+ ItemId INT,
+ Count INT
+);
+)""");
+ }
- fs::path dbPath = dbDir / "transactions.sqlite3";
-#if PLATFORM_WIN32
- if (int rc = sqlite3_open16(dbPath.c_str(), &mDatabase); rc) {
-#else
- if (int rc = sqlite3_open(transactionDbPath.c_str(), &mDatabase); rc) {
-#endif
- sqlite3_close(mDatabase);
+ // Schema
+ // - SendTime: unix epoch time of sending to delivery
+ // - ArriveTime: unix epoch time of delivery arrived at warehouse; 0 if not arrived yet
+ // - AssociatedOrder: rowid of the order that this delivery is completing (which table: Outgoing=true -> Sales, Outgoing=false -> Purchases)
+ // - Outgoing: true if the delivery is from warehouse to customer; false if the delivery is from factory to warehouse
+ if (!mDb.tableExists("Deliveries")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE Deliveries(
+ INT PRIMARY KEY,
+ SendTime DATETIME,
+ ArriveTime DATETIME,
+ AssociatedOrder INT,
+ Outgoing BOOLEAN
+);
+)""");
+ }
- std::string message;
- message += "Failed to open SQLite database for transactions. Error code: ";
- message += rc;
- message += ".";
- throw std::runtime_error(message);
+ if (!mDb.tableExists("DeliveriesItems")) {
+ // language=SQLite
+ mDb.exec(R"""(
+CREATE TABLE DeliveriesItems(
+ DeliveryId INT,
+ ItemId INT,
+ Count INT
+);
+)""");
}
}
-TransactionDatabase::~TransactionDatabase() {
- sqlite3_close(mDatabase);
- mDatabase = nullptr;
+const SQLite::Database& TransactionDatabase::GetSQLite() const {
+ return mDb;
+}
+
+SQLite::Database& TransactionDatabase::GetSQLite() {
+ return mDb;
+}
+
+const SalesTable& TransactionDatabase::GetSales() const {
+ return mSales;
+}
+
+SalesTable& TransactionDatabase::GetSales() {
+ return mSales;
+}
+
+const PurchasesTable& TransactionDatabase::GetPurchases() const {
+ return mPurchases;
+}
+
+PurchasesTable& TransactionDatabase::GetPurchases() {
+ return mPurchases;
+}
+
+const DeliveryTable& TransactionDatabase::GetDeliveries() const {
+ return mDeliveries;
+}
+
+DeliveryTable& TransactionDatabase::GetDeliveries() {
+ return mDeliveries;
}
diff --git a/core/src/Model/TransactionDatabase.hpp b/core/src/Model/TransactionDatabase.hpp
index 191a8b8..9c869c4 100644
--- a/core/src/Model/TransactionDatabase.hpp
+++ b/core/src/Model/TransactionDatabase.hpp
@@ -2,33 +2,49 @@
#include "cplt_fwd.hpp"
-#include <sqlite3.h>
+#include <SQLiteCpp/Database.h>
+#include <SQLiteCpp/Statement.h>
#include <cstdint>
-struct DeliveryId {
- int64_t id;
-};
+class SalesTable {
+public:
+ SQLite::Statement GetRowsStatement;
+ SQLite::Statement FilterRowsStatement;
-class DeliveryTable {
-};
+public:
+ SalesTable(TransactionDatabase& db);
-class OrdersTable {
+ int GetEntryCont() const;
};
class PurchasesTable {
+public:
+ PurchasesTable(TransactionDatabase& db);
+};
+
+class DeliveryTable {
+public:
+ DeliveryTable(TransactionDatabase& db);
};
class TransactionDatabase {
private:
Project* mProject;
- sqlite3* mDatabase;
+ SQLite::Database mDb;
+ SalesTable mSales;
+ PurchasesTable mPurchases;
+ DeliveryTable mDeliveries;
public:
TransactionDatabase(Project& project);
- ~TransactionDatabase();
- TransactionDatabase(const TransactionDatabase&) = delete;
- TransactionDatabase& operator=(const TransactionDatabase&) = delete;
- TransactionDatabase(TransactionDatabase&&) = default;
- TransactionDatabase& operator=(TransactionDatabase&&) = default;
+ const SQLite::Database& GetSQLite() const;
+ SQLite::Database& GetSQLite();
+
+ const SalesTable& GetSales() const;
+ SalesTable& GetSales();
+ const PurchasesTable& GetPurchases() const;
+ PurchasesTable& GetPurchases();
+ const DeliveryTable& GetDeliveries() const;
+ DeliveryTable& GetDeliveries()
};
diff --git a/core/src/Model/fwd.hpp b/core/src/Model/fwd.hpp
index 2d8d2ec..e153923 100644
--- a/core/src/Model/fwd.hpp
+++ b/core/src/Model/fwd.hpp
@@ -1,5 +1,8 @@
#pragma once
+// Filter.hpp
+class TableRowsFilter;
+
// GlobalStates.hpp
class GlobalStates;
@@ -16,4 +19,7 @@ class CustomerItem;
class Project;
// TransactionDatabase.hpp
+class SalesTable;
+class PurchasesTable;
+class DeliveryTable;
class TransactionDatabase;
diff --git a/core/src/UI/Localization.hpp b/core/src/UI/Localization.hpp
index 86b7afc..f476458 100644
--- a/core/src/UI/Localization.hpp
+++ b/core/src/UI/Localization.hpp
@@ -12,15 +12,23 @@ public:
static std::unique_ptr<LocaleStrings> Instance;
public:
+ /* Generic */
+
BasicTranslation Error{ "Generic.Error"sv };
+ BasicTranslation Add{ "Generic.Add"sv };
+ BasicTranslation Edit{ "Generic.Edit"sv };
+ BasicTranslation Delete{ "Generic.Delete"sv };
BasicTranslation DialogConfirm{ "Generic.Dialog.Confirm"sv };
BasicTranslation DialogCancel{ "Generic.Dialog.Cancel"sv };
- BasicTranslation TabSettings{ "MainWindow.Tab.Settings"sv };
- BasicTranslation TabProject{ "MainWindow.Tab.Project"sv };
- BasicTranslation TabDatabaseView{ "MainWindow.Tab.DatabaseView"sv };
- BasicTranslation TabItems{ "MainWindow.Tab.Items"sv };
- BasicTranslation TabExport{ "MainWindow.Tab.Exports"sv };
+ /* Main window */
+
+ BasicTranslation SettingsTab{ "MainWindow.Tab.Settings"sv };
+ BasicTranslation ProjectTab{ "MainWindow.Tab.Project"sv };
+ BasicTranslation DatabaseViewTab{ "MainWindow.Tab.DatabaseView"sv };
+ BasicTranslation ItemsTab{ "MainWindow.Tab.Items"sv };
+
+ /* Project tab */
BasicTranslation NewProject{ "Project.New"sv };
BasicTranslation NewProjectDialogTitle{ "Project.New.DialogTitle"sv };
@@ -46,11 +54,24 @@ public:
BasicTranslation ActiveProjectName{ "ActiveProject.Info.Name"sv };
BasicTranslation ActiveProjectPath{ "ActiveProject.Info.Path"sv };
- BasicTranslation AddItem{ "Item.Add"sv };
+ /* Database view tab */
+
+ BasicTranslation SalesViewTab{ "Database.SalesView.TabName"sv };
+ BasicTranslation EditSaleEntryDialogTitle{ "Database.SalesView.Edit.DialogTitle"sv };
+
+ BasicTranslation PurchasesViewTab{ "Database.PurchasesView.TabName"sv };
+
+ BasicTranslation DeliveriesViewTab{ "Database.DeliveriesView.TabName"sv };
+
+ BasicTranslation DatabaseCustomerColumn{ "Database.Column.Customer"sv };
+ BasicTranslation DatabaseDeadlineColumn{ "Database.Column.Deadline"sv };
+ BasicTranslation DatabaseDeliveryTimeColumn{ "Database.Column.DeliveryTime"sv };
+ BasicTranslation NotDeliveredMessage{ "Database.Message.NotDelivered"sv };
+
+ /* Items tab */
+
BasicTranslation AddItemDialogTitle{ "Item.Add.DialogTitle"sv };
- BasicTranslation EditItem{ "Item.Edit"sv };
BasicTranslation EditItemDialogTitle{ "Item.Edit.DialogTitle"sv };
- BasicTranslation DeleteItem{ "Item.Delete"sv };
BasicTranslation DeleteItemDialogTitle{ "Item.Delete.DialogTitle"sv };
BasicTranslation DeleteItemDialogMessage{ "Item.Delete.DialogMessage"sv };
@@ -61,6 +82,8 @@ public:
BasicTranslation ItemNameColumn{ "Item.Column.Name"sv };
BasicTranslation ItemDescriptionColumn{ "Item.Column.Description"sv };
BasicTranslation ItemEmailColumn{ "Item.Column.Email"sv };
+ BasicTranslation ItemStockColumn{ "Item.Column.Stock"sv };
+ BasicTranslation ItemPriceColumn{ "Item.Column.Price"sv };
BasicTranslation EmptyItemNameError{ "Item.EmptyNameError"sv };
BasicTranslation DuplicateItemNameError{ "Item.DuplicateNameError"sv };
diff --git a/core/src/UI/UI_DatabaseView.cpp b/core/src/UI/UI_DatabaseView.cpp
index 234aeaa..2b74918 100644
--- a/core/src/UI/UI_DatabaseView.cpp
+++ b/core/src/UI/UI_DatabaseView.cpp
@@ -1,9 +1,281 @@
#include "UI.hpp"
+#include "Model/Project.hpp"
#include "UI/Localization.hpp"
+#include "UI/States.hpp"
+#include "Utils/ScopeGuard.hpp"
+#include "cplt_fwd.hpp"
+#include <IconsFontAwesome.h>
+#include <SQLiteCpp/Statement.h>
#include <imgui.h>
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+namespace {
+
+// TODO move to Settings
+constexpr int kMaxEntriesPerPage = 20;
+
+class SaleEntry {
+public:
+ std::string Customer;
+ std::string Deadline;
+ std::string DeliveryTime;
+};
+
+class SalesViewTab {
+private:
+ Project* mProject;
+
+ /// Current active filter object, or \c nullptr.
+ std::unique_ptr<TableRowsFilter> mActiveFilter;
+
+ /// Inclusive.
+ /// \see mLastCachedRowId
+ int64_t mFirstCachedRowId;
+ /// Inclusive.
+ /// \see mFirstCachedRowId
+ int64_t mLastCachedRowId;
+
+ /// A cached, contiguous (row id of each entry is monotonically increasing, but not necessarily starts at 0) list ready-to-be-presented entries. May be incomplete.
+ std::vector<SaleEntry> mEntries;
+
+ // TODO this is very impractical (running filter in client code instead of letting SQLite doing it)
+ // Maybe simply cache a list of indices produced by a sql query?
+
+ /// A bitset of all active (should-be-displayed) entries based on current filter, updated whenever \c mActiveFilter is updated.
+ /// Should have the exact same size as \c entries.
+ std::vector<bool> mActiveEntries;
+
+ /// A cached list of index to entries that should be displayed on the current page.
+ std::vector<int> mCurrentPageEntries;
+
+ /// The current page the user is on.
+ int mCurrentPage;
+ /// Last possible page for the current set table and filter (inclusive).
+ int mLastPage;
+
+ /* UI states */
+
+ int mSelectedEntry;
+
+public:
+ void OnProjectChanged(Project* newProject) {
+ mProject = newProject;
+
+ mEntries.clear();
+ mCurrentPage = 0;
+ mLastPage = -1;
+
+ mSelectedEntry = -1;
+ }
+
+ void OnFilterChanged(std::unique_ptr<TableRowsFilter> filter) {
+ // TODO
+ }
+
+ void Draw() {
+ bool dummy = true;
+ auto ls = LocaleStrings::Instance.get();
+
+ if (ImGui::Button(ICON_FA_ARROW_LEFT, mCurrentPage == 0)) {
+ SetPage(mCurrentPage - 1);
+ }
+
+ ImGui::SameLine();
+ // +1 to convert from 0-based indices to 1-based, for human legibility
+ ImGui::Text("%d/%d", mCurrentPage + 1, mLastPage + 1);
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_ARROW_RIGHT, mCurrentPage == mLastPage)) {
+ SetPage(mCurrentPage + 1);
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ls->Edit.Get(), mSelectedEntry == -1)) {
+ ImGui::OpenPopup(ls->EditSaleEntryDialogTitle.Get());
+ }
+ if (ImGui::BeginPopupModal(ls->EditSaleEntryDialogTitle.Get(), &dummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ // TODO
+ ImGui::EndPopup();
+ }
+
+ if (ImGui::BeginTable("##SalesTable", 3)) {
+
+ ImGui::TableSetupColumn(ls->DatabaseCustomerColumn.Get());
+ ImGui::TableSetupColumn(ls->DatabaseDeadlineColumn.Get());
+ ImGui::TableSetupColumn(ls->DatabaseDeliveryTimeColumn.Get());
+ ImGui::TableHeadersRow();
+
+ for (int i : mCurrentPageEntries) {
+ auto& entry = mEntries[i];
+
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(entry.Customer.c_str(), mSelectedEntry == i)) {
+ mSelectedEntry = i;
+ }
+
+ ImGui::NextColumn();
+ ImGui::Text("%s", entry.Deadline.c_str());
+
+ ImGui::NextColumn();
+ if (entry.DeliveryTime.empty()) {
+ ImGui::Text("%s", ls->NotDeliveredMessage.Get());
+ } else {
+ ImGui::Text("%s", entry.DeliveryTime.c_str());
+ }
+ }
+
+ ImGui::EndTable();
+ }
+ }
+
+private:
+ void SetPage(int page) {
+ if (mCurrentPage != page) return;
+
+ mCurrentPage = page;
+ EnsureCacheCoversPage(page);
+
+ auto [begin, end] = CalcRangeForPage(page);
+ begin -= mFirstCachedRowId;
+ end -= mFirstCachedRowId;
+
+ mCurrentPageEntries.clear();
+ for (auto i = begin; i < end; ++i) {
+ if (mActiveEntries[i]) {
+ mCurrentPageEntries.push_back(i);
+ }
+ }
+ }
+
+ void EnsureCacheCoversPage(int page) {
+ auto [begin, end] = CalcRangeForPage(page);
+ EnsureCacheCovers(begin, end - 1);
+ }
+
+ void EnsureCacheCovers(int64_t firstRow, int64_t lastRow) {
+ if (firstRow > lastRow) {
+ std::swap(firstRow, lastRow);
+ }
+
+ int newFirst = mFirstCachedRowId;
+ int newLast = mLastCachedRowId;
+
+ bool doRebuild = false;
+ if (firstRow < mFirstCachedRowId) {
+ newFirst = CalcPageForRowId(firstRow) * kMaxEntriesPerPage;
+ doRebuild = true;
+ }
+ if (lastRow > mLastCachedRowId) {
+ newLast = CalcPageForRowId(lastRow) * kMaxEntriesPerPage;
+ doRebuild = true;
+ }
+ if (!doRebuild) return;
+
+ auto front = LoadRange(newFirst, mFirstCachedRowId);
+ auto back = LoadRange(mLastCachedRowId + 1, newLast + 1);
+
+ mEntries.insert(mEntries.begin(), front.begin(), front.end());
+ mEntries.insert(mEntries.end(), back.begin(), back.end());
+ // TODO update mActiveEntries
+
+ mFirstCachedRowId = newFirst;
+ mLastCachedRowId = newLast;
+ }
+
+ std::vector<SaleEntry> LoadRange(int64_t begin, int64_t end) {
+ std::vector<SaleEntry> result;
+
+ size_t size = end - begin;
+ if (size == 0) {
+ return result;
+ }
+
+ result.reserve(size);
+
+ auto& stmt = mProject->GetDatabase().GetSales().GetRowsStatement;
+ DEFER {
+ stmt.reset();
+ };
+
+ stmt.bind(1, begin);
+ stmt.bind(2, end);
+
+ while (true) {
+ bool hasResult = stmt.executeStep();
+ if (hasResult) {
+ result.push_back(stmt.getColumns<SaleEntry, 3>());
+ } else {
+ return result;
+ }
+ }
+ }
+
+ static int CalcPageForRowId(int64_t rowId) {
+ return rowId / kMaxEntriesPerPage;
+ }
+
+ /// Calculate range [begin, end) of row ids that the path-th page would show.
+ static std::pair<int64_t, int64_t> CalcRangeForPage(int page) {
+ int begin = page * kMaxEntriesPerPage;
+ return { begin, begin + kMaxEntriesPerPage };
+ }
+};
+
+class PurchaseEntry {
+public:
+ std::string Factory;
+ std::string OrderTime;
+ std::string DeliveryTime;
+};
+
+class PurchasesViewTab {
+private:
+ std::vector<PurchaseEntry> mEntries;
+ int mCurrentPage = 0;
+
+public:
+ void OnProjectChanged(Project* newProject) {
+ // TODO
+ }
+
+ void Draw() {
+ auto ls = LocaleStrings::Instance.get();
+ if (ImGui::BeginTable("##PurcahsesTable", 4)) {
+ ImGui::EndTable();
+ }
+ }
+};
+} // namespace
void UI::DatabaseViewTab() {
- // TODO
+ auto ls = LocaleStrings::Instance.get();
+ auto& uis = UIState::GetInstance();
+
+ static Project* currentProject = nullptr;
+ static auto salesView = std::make_unique<SalesViewTab>();
+ static auto purchasesView = std::make_unique<PurchasesViewTab>();
+
+ if (currentProject != uis.CurrentProject.get()) {
+ currentProject = uis.CurrentProject.get();
+ salesView->OnProjectChanged(currentProject);
+ purchasesView->OnProjectChanged(currentProject);
+ }
+
+ if (ImGui::BeginTabBar("##DatabaseViewTabs")) {
+ if (ImGui::BeginTabItem(ls->SalesViewTab.Get())) {
+ salesView->Draw();
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem(ls->PurchasesViewTab.Get())) {
+ purchasesView->Draw();
+ ImGui::EndTabItem();
+ }
+ // if (ImGui::BeginTabItem(ls->DeliveriesTableTab.Get())) {
+ // ImGui::EndTabItem();
+ // }
+ }
}
diff --git a/core/src/UI/UI_Items.cpp b/core/src/UI/UI_Items.cpp
index 970d0df..a094f76 100644
--- a/core/src/UI/UI_Items.cpp
+++ b/core/src/UI/UI_Items.cpp
@@ -91,6 +91,8 @@ template <class T>
void ItemListEntries(ItemList<T>& list, int& selectedIdx) {
constexpr bool kHasDescription = requires(T t) { t.GetDescription(); };
constexpr bool kHasEmail = requires(T t) { t.GetEmail(); };
+ constexpr bool kHasStock = requires(T t) { t.GetPrice(); };
+ constexpr bool kHasPrice = requires(T t) { t.GetPrice(); };
constexpr int kColumns = 1 /* Name column */ + kHasDescription + kHasEmail;
auto ls = LocaleStrings::Instance.get();
@@ -101,6 +103,8 @@ void ItemListEntries(ItemList<T>& list, int& selectedIdx) {
ImGui::TableSetupColumn(ls->ItemNameColumn.Get());
if constexpr (kHasDescription) ImGui::TableSetupColumn(ls->ItemDescriptionColumn.Get());
if constexpr (kHasEmail) ImGui::TableSetupColumn(ls->ItemEmailColumn.Get());
+ if constexpr (kHasStock) ImGui::TableSetupColumn(ls->ItemStockColumn.Get());
+ if constexpr (kHasPrice) ImGui::TableSetupColumn(ls->ItemPriceColumn.Get());
ImGui::TableHeadersRow();
size_t idx = 0;
@@ -111,24 +115,32 @@ void ItemListEntries(ItemList<T>& list, int& selectedIdx) {
ImGui::TableNextRow();
- // Field: name
ImGui::TableNextColumn();
if (ImGui::Selectable(entry.GetName().c_str(), selectedIdx == idx, ImGuiSelectableFlags_SpanAllColumns)) {
selectedIdx = idx;
}
- // Field: description
if constexpr (kHasDescription) {
ImGui::TableNextColumn();
ImGui::Text("%s", entry.GetDescription().c_str());
}
- // Field: email
if constexpr (kHasEmail) {
ImGui::TableNextColumn();
ImGui::Text("%s", entry.GetEmail().c_str());
}
+ if constexpr (kHasStock) {
+ ImGui::TableNextColumn();
+ ImGui::Text("%d", entry.GetStock());
+ }
+
+ if constexpr (kHasPrice) {
+ ImGui::TableNextColumn();
+ // TODO format in dollars
+ ImGui::Text("%d", entry.GetPrice());
+ }
+
idx++;
}
ImGui::EndTable();
@@ -143,7 +155,7 @@ void ItemListEditor(ItemList<T>& list) {
static int selectedIdx = -1;
static T* editingItem = nullptr;
- if (ImGui::Button(ls->AddItem.Get())) {
+ if (ImGui::Button(ls->Add.Get())) {
ImGui::SetNextWindowCentered();
ImGui::OpenPopup(ls->AddItemDialogTitle.Get());
@@ -165,7 +177,7 @@ void ItemListEditor(ItemList<T>& list) {
}
ImGui::SameLine();
- if (ImGui::Button(ls->EditItem.Get(), selectedIdx == -1)) {
+ if (ImGui::Button(ls->Edit.Get(), selectedIdx == -1)) {
ImGui::SetNextWindowCentered();
ImGui::OpenPopup(ls->EditItemDialogTitle.Get());
@@ -179,7 +191,7 @@ void ItemListEditor(ItemList<T>& list) {
}
ImGui::SameLine();
- if (ImGui::Button(ls->DeleteItem.Get(), selectedIdx == -1)) {
+ if (ImGui::Button(ls->Delete.Get(), selectedIdx == -1)) {
ImGui::SetNextWindowCentered();
ImGui::OpenPopup(ls->DeleteItemDialogTitle.Get());
diff --git a/core/src/UI/UI_MainWindow.cpp b/core/src/UI/UI_MainWindow.cpp
index a47efab..30505d6 100644
--- a/core/src/UI/UI_MainWindow.cpp
+++ b/core/src/UI/UI_MainWindow.cpp
@@ -195,12 +195,12 @@ void UI::MainWindow() {
ImGui::SetNextWindowPos({ 0, 0 });
ImGui::Begin("##MainWindow", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize);
if (ImGui::BeginTabBar("##MainWindowTabs")) {
- if (ImGui::BeginTabItem(ls->TabSettings.Get())) {
+ if (ImGui::BeginTabItem(ls->SettingsTab.Get())) {
UI::SettingsTab();
ImGui::EndTabItem();
}
- if (ImGui::BeginTabItem(ls->TabProject.Get(), nullptr)) {
+ if (ImGui::BeginTabItem(ls->ProjectTab.Get(), nullptr)) {
if (uis.CurrentProject) {
ProjectTab_Normal();
} else {
@@ -213,21 +213,16 @@ void UI::MainWindow() {
goto endTab;
}
- if (ImGui::BeginTabItem(ls->TabDatabaseView.Get())) {
+ if (ImGui::BeginTabItem(ls->DatabaseViewTab.Get())) {
UI::DatabaseViewTab();
ImGui::EndTabItem();
}
- if (ImGui::BeginTabItem(ls->TabItems.Get())) {
+ if (ImGui::BeginTabItem(ls->ItemsTab.Get())) {
UI::ItemsTab();
ImGui::EndTabItem();
}
- if (ImGui::BeginTabItem(ls->TabExport.Get())) {
- UI::ExportTab();
- ImGui::EndTabItem();
- }
-
endTab:
ImGui::EndTabBar();
}
diff --git a/core/src/Utils/Macros.hpp b/core/src/Utils/Macros.hpp
new file mode 100644
index 0000000..cb949b2
--- /dev/null
+++ b/core/src/Utils/Macros.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#define CONCAT_IMPL(a, b) a##b
+#define CONCAT(a, b) CONCAT_IMPL(a, b)
+
+#define UNIQUE_NAME(prefix) CONCAT(prefix, __LINE__)
diff --git a/core/src/Utils/ScopeGuard.hpp b/core/src/Utils/ScopeGuard.hpp
new file mode 100644
index 0000000..ed8d4ea
--- /dev/null
+++ b/core/src/Utils/ScopeGuard.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "Utils/Macros.hpp"
+
+#include <utility>
+
+template <class TCleanupFunc>
+class ScopeGuard {
+private:
+ TCleanupFunc mFunc;
+ bool mDismissed = false;
+
+public:
+ /// Specifically left this implicit so that constructs like
+ /// \code
+ /// ScopeGuard sg = [&]() { res.Cleanup(); };
+ /// \endcode
+ /// would work. It is highly discourage and unlikely that one would want to use ScopeGuard as a function
+ /// parameter, so the normal argument that implicit conversion are harmful doesn't really apply here.
+ ScopeGuard(TCleanupFunc func)
+ : mFunc{ std::move(func) } {
+ }
+
+ ~ScopeGuard() {
+ if (!mDismissed) {
+ mFunc();
+ }
+ }
+
+ void Dismiss() noexcept {
+ mDismissed = true;
+ }
+};
+
+#define DEFER ScopeGuard UNIQUE_NAME(scopeGuard) = [&]()