diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/CMakeLists.txt | 1 | ||||
-rw-r--r-- | core/locale/zh_CN.json | 17 | ||||
-rw-r--r-- | core/src/Model/Filter.cpp | 1 | ||||
-rw-r--r-- | core/src/Model/Filter.hpp | 5 | ||||
-rw-r--r-- | core/src/Model/Items.cpp | 9 | ||||
-rw-r--r-- | core/src/Model/Items.hpp | 6 | ||||
-rw-r--r-- | core/src/Model/Project.cpp | 8 | ||||
-rw-r--r-- | core/src/Model/Project.hpp | 3 | ||||
-rw-r--r-- | core/src/Model/TransactionDatabase.cpp | 161 | ||||
-rw-r--r-- | core/src/Model/TransactionDatabase.hpp | 42 | ||||
-rw-r--r-- | core/src/Model/fwd.hpp | 6 | ||||
-rw-r--r-- | core/src/UI/Localization.hpp | 39 | ||||
-rw-r--r-- | core/src/UI/UI_DatabaseView.cpp | 274 | ||||
-rw-r--r-- | core/src/UI/UI_Items.cpp | 24 | ||||
-rw-r--r-- | core/src/UI/UI_MainWindow.cpp | 13 | ||||
-rw-r--r-- | core/src/Utils/Macros.hpp | 6 | ||||
-rw-r--r-- | core/src/Utils/ScopeGuard.hpp | 35 |
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) = [&]() |