aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/UI/UI_DatabaseView.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'app/source/Cplt/UI/UI_DatabaseView.cpp')
-rw-r--r--app/source/Cplt/UI/UI_DatabaseView.cpp668
1 files changed, 668 insertions, 0 deletions
diff --git a/app/source/Cplt/UI/UI_DatabaseView.cpp b/app/source/Cplt/UI/UI_DatabaseView.cpp
new file mode 100644
index 0000000..1e58eb0
--- /dev/null
+++ b/app/source/Cplt/UI/UI_DatabaseView.cpp
@@ -0,0 +1,668 @@
+#include "UI.hpp"
+
+#include <Cplt/Model/Filter.hpp>
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/Utils/I18n.hpp>
+#include <Cplt/Utils/ScopeGuard.hpp>
+#include <Cplt/Utils/Time.hpp>
+
+#include <IconsFontAwesome.h>
+#include <SQLiteCpp/Statement.h>
+#include <imgui.h>
+#include <tsl/robin_map.h>
+#include <cstdint>
+#include <iostream>
+#include <memory>
+#include <vector>
+
+namespace CPLT_UNITY_ID {
+
+// TODO move to Settings
+constexpr int kMaxEntriesPerPage = 32;
+constexpr int kSummaryItemCount = 3;
+constexpr int kSummaryMaxLength = 25;
+
+std::pair<int, int> SplitEntryIndex(int entryIdx)
+{
+ int page = entryIdx / kMaxEntriesPerPage;
+ int row = entryIdx % kMaxEntriesPerPage;
+ return { page, row };
+}
+
+enum class DeliveryDirection
+{
+ FactoryToWarehouse,
+ WarehouseToCustomer,
+};
+
+struct Item
+{
+ int ItemId;
+ int Count;
+};
+
+struct DeliveryEntry
+{
+ std::vector<Item> Items;
+ std::string ItemsSummary;
+ std::string ShipmentTime;
+ std::string ArriveTime;
+ DeliveryDirection Direction;
+
+ const char* StringifyDirection() const
+ {
+ switch (Direction) {
+ case DeliveryDirection::FactoryToWarehouse: return "Factory to warehouse";
+ case DeliveryDirection::WarehouseToCustomer: return "Warehouse to customer";
+ }
+ }
+};
+
+struct SaleEntry
+{
+ static constexpr auto kType = DeliveryDirection::WarehouseToCustomer;
+
+ std::vector<DeliveryEntry> AssociatedDeliveries;
+ std::vector<Item> Items;
+ std::string ItemsSummary;
+ std::string Customer;
+ std::string Deadline;
+ std::string DeliveryTime;
+ int Id;
+ bool DeliveriesCached = false;
+};
+
+struct PurchaseEntry
+{
+ static constexpr auto kType = DeliveryDirection::FactoryToWarehouse;
+
+ std::vector<DeliveryEntry> AssociatedDeliveries;
+ std::vector<Item> Items;
+ std::string ItemsSummary;
+ std::string Factory;
+ std::string OrderTime;
+ std::string DeliveryTime;
+ int Id;
+ bool DeliveriesCached;
+};
+
+template <class T>
+class GenericTableView
+{
+public:
+ // clang-format off
+ static constexpr bool kHasItems = requires(T t)
+ {
+ t.Items;
+ t.ItemsSummary;
+ };
+ static constexpr bool kHasCustomer = requires(T t) { t.Customer; };
+ static constexpr bool kHasDeadline = requires(T t) { t.Deadline; };
+ static constexpr bool kHasFactory = requires(T t) { t.Factory; };
+ static constexpr bool kHasOrderTime = requires(T t) { t.OrderTime; };
+ static constexpr bool kHasCompletionTime = requires(T t) { t.DeliveryTime; };
+ static constexpr int kColumnCount = kHasItems + kHasCustomer + kHasDeadline + kHasFactory + kHasOrderTime + kHasCompletionTime;
+ // clang-format on
+
+ using Page = std::vector<T>;
+
+ struct QueryStatements
+ {
+ SQLite::Statement* GetRowCount;
+ SQLite::Statement* GetRows;
+ SQLite::Statement* GetItems;
+ SQLite::Statement* FilterRows;
+ } Statements;
+
+protected:
+ // Translation entries for implementer to fill out
+ const char* mEditDialogTitle;
+
+ Project* mProject;
+ Page* mCurrentPage = nullptr;
+
+ /// Current active filter object, or \c nullptr.
+ std::unique_ptr<TableRowsFilter> mActiveFilter;
+
+ tsl::robin_map<int, Page> mPages;
+
+ /// A vector of entry indices (in \c mEntries) that are visible under the current filter.
+ std::vector<int> mActiveEntries;
+
+ /// Number of rows in the table.
+ int mRowCount;
+ /// Last possible page for the current set table and filter (inclusive).
+ int mLastPage;
+
+ /// The current page the user is on.
+ int mCurrentPageNumber;
+
+ /// Row index of the select entry
+ int mSelectRow;
+
+public:
+ /// Calculate the first visible row's entry index.
+ int GetFirstVisibleRowIdx() const
+ {
+ return mCurrentPageNumber * kMaxEntriesPerPage;
+ }
+
+ Project* GetProject() const
+ {
+ return mProject;
+ }
+
+ void OnProjectChanged(Project* newProject)
+ {
+ mProject = newProject;
+
+ auto& stmt = *Statements.GetRowCount;
+ if (stmt.executeStep()) {
+ mRowCount = stmt.getColumn(0).getInt();
+ } else {
+ std::cerr << "Failed to fetch row count from SQLite.\n";
+ mRowCount = 0;
+ }
+
+ mActiveFilter = nullptr;
+ mActiveEntries.clear();
+
+ mPages.clear();
+ mCurrentPage = nullptr;
+ UpdateLastPage();
+ SetPage(0);
+
+ mSelectRow = -1;
+ }
+
+ TableRowsFilter* GetFilter() const
+ {
+ return mActiveFilter.get();
+ }
+
+ void OnFilterChanged()
+ {
+ auto& stmt = *Statements.FilterRows;
+ // clang-format off
+ DEFER { stmt.reset(); };
+ // clang-format on
+
+ // TODO lazy loading when too many results
+ mActiveEntries.clear();
+ int columnIdx = stmt.getColumnIndex("Id");
+ while (stmt.executeStep()) {
+ mActiveEntries.push_back(stmt.getColumn(columnIdx).getInt());
+ }
+
+ UpdateLastPage();
+ SetPage(0);
+
+ mSelectRow = -1;
+ }
+
+ void OnFilterChanged(std::unique_ptr<TableRowsFilter> filter)
+ {
+ mActiveFilter = std::move(filter);
+ OnFilterChanged();
+ }
+
+ void Display()
+ {
+ bool dummy = true;
+
+ if (ImGui::Button(ICON_FA_ARROW_LEFT, mCurrentPageNumber == 0)) {
+ SetPage(mCurrentPageNumber - 1);
+ }
+
+ ImGui::SameLine();
+ // +1 to convert from 0-based indices to 1-based, for human legibility
+ ImGui::Text("%d/%d", mCurrentPageNumber + 1, mLastPage + 1);
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_ARROW_RIGHT, mCurrentPageNumber == mLastPage)) {
+ SetPage(mCurrentPageNumber + 1);
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_PLUS " " I18N_TEXT("Add", L10N_ADD))) {
+ // TODO
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_EDIT " " I18N_TEXT("Edit", L10N_EDIT), mSelectRow == -1)) {
+ ImGui::OpenPopup(mEditDialogTitle);
+ }
+ if (ImGui::BeginPopupModal(mEditDialogTitle, &dummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ auto& entry = (*mCurrentPage)[mSelectRow];
+ int entryIdx = GetFirstVisibleRowIdx() + mSelectRow;
+ EditEntry(entry, entryIdx, mSelectRow);
+ ImGui::EndPopup();
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_TRASH " " I18N_TEXT("Delete", L10N_DELETE), mSelectRow == -1)) {
+ // TODO
+ }
+
+ ImGui::Columns(2);
+ {
+ DisplayMainTable();
+ ImGui::NextColumn();
+
+ if (mSelectRow == -1) {
+ ImGui::TextWrapped("%s", I18N_TEXT("Select an entry to show associated deliveries", L10N_DATABASE_MESSAGE_NO_ORDER_SELECTED));
+ } else {
+ DisplayDeliveriesTable();
+ }
+ ImGui::NextColumn();
+ }
+ ImGui::Columns(1);
+ }
+
+ void SetPage(int page)
+ {
+ mCurrentPageNumber = page;
+ mCurrentPage = &LoadAndGetPage(page);
+ mSelectRow = -1;
+ }
+
+private:
+ static int CalcPageForRowId(int64_t entryIdx)
+ {
+ return entryIdx / kMaxEntriesPerPage;
+ }
+
+ /// Calculate range [begin, end) of index for the list of entries that are currently visible that the path-th page would show.
+ /// i.e. when there is a filter, look into \c mActiveEntryIndices; when there is no filter, use directly.
+ static std::pair<int64_t, int64_t> CalcRangeForPage(int page)
+ {
+ int begin = page * kMaxEntriesPerPage;
+ return { begin, begin + kMaxEntriesPerPage };
+ }
+
+ void DisplayMainTable()
+ {
+ if (ImGui::BeginTable("DataTable", kColumnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) {
+
+ if constexpr (kHasCustomer) ImGui::TableSetupColumn(I18N_TEXT("Customer", L10N_DATABASE_COLUMN_CUSTOMER));
+ if constexpr (kHasDeadline) ImGui::TableSetupColumn(I18N_TEXT("Deadline", L10N_DATABASE_COLUMN_DEADLINE));
+ if constexpr (kHasFactory) ImGui::TableSetupColumn(I18N_TEXT("Factory", L10N_DATABASE_COLUMN_FACTORY));
+ if constexpr (kHasOrderTime) ImGui::TableSetupColumn(I18N_TEXT("Order time", L10N_DATABASE_COLUMN_ORDER_TIME));
+ if constexpr (kHasCompletionTime) ImGui::TableSetupColumn(I18N_TEXT("Completion time", L10N_DATABASE_COLUMN_DELIVERY_TIME));
+ if constexpr (kHasItems) ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS));
+ ImGui::TableHeadersRow();
+
+ if (mActiveFilter) {
+ // TODO
+ auto [begin, end] = CalcRangeForPage(mCurrentPageNumber);
+ end = std::min(end, (int64_t)mActiveEntries.size() - 1);
+ for (int i = begin; i < end; ++i) {
+ int rowIdx = mActiveEntries[i];
+ DisplayEntry(rowIdx);
+ }
+ } else {
+ int firstRowIdx = mCurrentPageNumber * kMaxEntriesPerPage;
+ auto& page = *mCurrentPage;
+
+ int end = std::min((int)page.size(), kMaxEntriesPerPage);
+ for (int i = 0; i < end; ++i) {
+ DisplayEntry(page[i], i, firstRowIdx + i);
+ }
+ }
+
+ ImGui::EndTable();
+ }
+ }
+
+ void DisplayEntry(int rowIdx)
+ {
+ // TODO
+ // auto [pageNumber, pageEntry] = SplitRowIndex(rowIdx);
+ // auto& entry = LoadAndGetPage(pageNumber)[pageEntry];
+ // DisplayEntry(entry, rowIdx);
+ }
+
+ void DisplayEntry(T& entry, int rowIdx, int entryIdx)
+ {
+ ImGui::PushID(rowIdx);
+ ImGui::TableNextRow();
+
+ if constexpr (kHasCustomer) {
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(entry.Customer.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) {
+ mSelectRow = rowIdx;
+ }
+ }
+
+ if constexpr (kHasDeadline) {
+ ImGui::TableNextColumn();
+ ImGui::TextUnformatted(entry.Deadline.c_str());
+ }
+
+ if constexpr (kHasFactory) {
+ ImGui::TableNextColumn();
+ if (ImGui::Selectable(entry.Factory.c_str(), mSelectRow == rowIdx, ImGuiSelectableFlags_SpanAllColumns)) {
+ mSelectRow = rowIdx;
+ }
+ }
+
+ if constexpr (kHasOrderTime) {
+ ImGui::TableNextColumn();
+ ImGui::TextUnformatted(entry.OrderTime.c_str());
+ }
+
+ if constexpr (kHasCompletionTime) {
+ ImGui::TableNextColumn();
+ if (entry.DeliveryTime.empty()) {
+ ImGui::TextUnformatted(I18N_TEXT("Not delivered", L10N_DATABASE_MESSAGE_NOT_DELIVERED));
+ } else {
+ ImGui::TextUnformatted(entry.DeliveryTime.c_str());
+ }
+ }
+
+ if constexpr (kHasItems) {
+ ImGui::TableNextColumn();
+ if (ImGui::TreeNode(entry.ItemsSummary.c_str())) {
+ DrawItems(entry.Items);
+ ImGui::TreePop();
+ }
+ }
+
+ ImGui::PopID();
+ }
+
+ void EditEntry(T& entry, int rowIdx, int entryIdx)
+ {
+ // TODO
+ }
+
+ void DisplayDeliveriesTable()
+ {
+ if (ImGui::BeginTable("DeliveriesTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) {
+
+ ImGui::TableSetupColumn(I18N_TEXT("Shipment time", L10N_DATABASE_COLUMN_SHIPMENT_TIME));
+ ImGui::TableSetupColumn(I18N_TEXT("Arrival time", L10N_DATABASE_COLUMN_ARRIVAL_TIME));
+ ImGui::TableSetupColumn(I18N_TEXT("Items", L10N_DATABASE_COLUMN_ITEMS));
+ ImGui::TableHeadersRow();
+
+ auto& entry = (*mCurrentPage)[mSelectRow];
+ auto& deliveries = entry.AssociatedDeliveries;
+ for (auto& delivery : deliveries) {
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+ ImGui::TextUnformatted(delivery.ShipmentTime.c_str());
+
+ ImGui::TableNextColumn();
+ ImGui::TextUnformatted(delivery.ArriveTime.c_str());
+
+ ImGui::TableNextColumn();
+ if (ImGui::TreeNode(delivery.ItemsSummary.c_str())) {
+ DrawItems(delivery.Items);
+ ImGui::TreePop();
+ }
+ }
+
+ ImGui::EndTable();
+ }
+ }
+
+ std::vector<Item> LoadItems(SQLite::Statement& stmt, int64_t id)
+ {
+ // clang-format off
+ DEFER { stmt.reset(); };
+ // clang-format on
+
+ stmt.bind(1, id);
+
+ std::vector<Item> entries;
+ int itemIdCol = stmt.getColumnIndex("ItemId");
+ int countCol = stmt.getColumnIndex("Count");
+ while (stmt.executeStep()) {
+ entries.push_back(Item{
+ .ItemId = stmt.getColumn(itemIdCol).getInt(),
+ .Count = stmt.getColumn(countCol).getInt(),
+ });
+ }
+
+ return entries;
+ }
+
+ std::string CreateItemsSummary(const std::vector<Item>& items)
+ {
+ if (items.empty()) {
+ return "<empty>";
+ }
+
+ std::string result;
+ for (int i = 0, max = std::min((int)items.size(), kSummaryItemCount); i < max; ++i) {
+ auto& name = mProject->Products.Find(items[i].ItemId)->GetName();
+ if (result.length() + name.length() > kSummaryMaxLength) {
+ break;
+ }
+
+ result += name;
+ result += ", ";
+ }
+
+ // Remove ", "
+ result.pop_back();
+ result.pop_back();
+
+ result += "...";
+
+ return result;
+ }
+
+ std::vector<DeliveryEntry> LoadDeliveriesEntries(int64_t orderId, DeliveryDirection type)
+ {
+ bool outgoingFlag;
+ switch (type) {
+ case DeliveryDirection::FactoryToWarehouse: outgoingFlag = false; break;
+ case DeliveryDirection::WarehouseToCustomer: outgoingFlag = true; break;
+ }
+
+ auto& stmt = mProject->Database.GetDeliveries().FilterByTypeAndId;
+ // clang-format off
+ DEFER { stmt.reset(); };
+ // clang-format on
+
+ stmt.bind(1, orderId);
+ stmt.bind(2, outgoingFlag);
+
+ std::vector<DeliveryEntry> entries;
+ int rowIdCol = stmt.getColumnIndex("Id");
+ int sendTimeCol = stmt.getColumnIndex("ShipmentTime");
+ int arrivalTimeCol = stmt.getColumnIndex("ArrivalTime");
+ while (stmt.executeStep()) {
+ auto items = LoadItems(
+ mProject->Database.GetDeliveries().GetItems,
+ stmt.getColumn(rowIdCol).getInt64());
+ auto summary = CreateItemsSummary(items);
+
+ entries.push_back(DeliveryEntry{
+ .Items = std::move(items),
+ .ItemsSummary = std::move(summary),
+ .ShipmentTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(arrivalTimeCol).getInt64()),
+ .ArriveTime = TimeUtils::StringifyTimeStamp(stmt.getColumn(sendTimeCol).getInt64()),
+ .Direction = type,
+ });
+ }
+
+ return entries;
+ }
+
+ Page& LoadAndGetPage(int page)
+ {
+ auto iter = mPages.find(page);
+ if (iter != mPages.end()) {
+ return iter.value();
+ }
+
+ auto& stmt = *Statements.GetRows;
+ // clang-format off
+ DEFER { stmt.reset(); };
+ // clang-format on
+
+ stmt.bind(1, kMaxEntriesPerPage);
+ stmt.bind(2, page * kMaxEntriesPerPage);
+
+ // If a field flag is false, the column index won't be used (controlled by other if constexpr's downstream)
+ // so there is no UB here
+
+ // This column is always necessary (and present) because the deliveries table require it
+ int idCol = stmt.getColumnIndex("Id");
+
+ int customerCol;
+ if constexpr (kHasCustomer) customerCol = stmt.getColumnIndex("Customer");
+
+ int deadlineCol;
+ if constexpr (kHasDeadline) deadlineCol = stmt.getColumnIndex("Deadline");
+
+ int factoryCol;
+ if constexpr (kHasFactory) factoryCol = stmt.getColumnIndex("Factory");
+
+ int orderTimeCol;
+ if constexpr (kHasOrderTime) orderTimeCol = stmt.getColumnIndex("OrderTime");
+
+ int deliveryTimeCol;
+ if constexpr (kHasCompletionTime) deliveryTimeCol = stmt.getColumnIndex("DeliveryTime");
+
+ Page entries;
+ while (stmt.executeStep()) {
+ auto& entry = entries.emplace_back();
+
+ auto id = stmt.getColumn(idCol).getInt64();
+ entry.AssociatedDeliveries = LoadDeliveriesEntries(id, T::kType);
+
+ if constexpr (kHasItems) {
+ auto items = LoadItems(
+ *Statements.GetItems,
+ id);
+ auto itemsSummary = CreateItemsSummary(items);
+ entry.Items = std::move(items);
+ entry.ItemsSummary = std::move(itemsSummary);
+ }
+
+ if constexpr (kHasCustomer) {
+ auto customerId = stmt.getColumn(customerCol).getInt();
+ entry.Customer = mProject->Customers.Find(customerId)->GetName();
+ }
+
+ if constexpr (kHasDeadline) {
+ auto timeStamp = stmt.getColumn(deadlineCol).getInt64();
+ entry.Deadline = TimeUtils::StringifyTimeStamp(timeStamp);
+ }
+
+ if constexpr (kHasFactory) {
+ auto factoryId = stmt.getColumn(factoryCol).getInt();
+ entry.Factory = mProject->Factories.Find(factoryId)->GetName();
+ }
+
+ if constexpr (kHasOrderTime) {
+ auto timeStamp = stmt.getColumn(orderTimeCol).getInt64();
+ entry.OrderTime = TimeUtils::StringifyTimeStamp(timeStamp);
+ }
+
+ if constexpr (kHasCompletionTime) {
+ auto timeStamp = stmt.getColumn(deliveryTimeCol).getInt64();
+ entry.DeliveryTime = TimeUtils::StringifyTimeStamp(timeStamp);
+ }
+ }
+
+ auto [res, _] = mPages.try_emplace(page, std::move(entries));
+ return res.value();
+ }
+
+ void DrawItems(const std::vector<Item>& items)
+ {
+ for (auto& item : items) {
+ auto& name = mProject->Products.Find(item.ItemId)->GetName();
+ ImGui::Text("%s × %d", name.c_str(), item.Count);
+ }
+ }
+
+ void UpdateLastPage()
+ {
+ mLastPage = mActiveEntries.empty()
+ ? CalcPageForRowId(mRowCount)
+ : CalcPageForRowId(mActiveEntries.back());
+ }
+};
+
+class SalesTableView : public GenericTableView<SaleEntry>
+{
+public:
+ SalesTableView()
+ {
+ mEditDialogTitle = I18N_TEXT("Edit sales entry", L10N_DATABASE_SALES_VIEW_EDIT_DIALOG_TITLE);
+ }
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "HidingNonVirtualFunction"
+ void OnProjectChanged(Project* newProject)
+ {
+ auto& table = newProject->Database.GetSales();
+ Statements.GetRowCount = &table.GetRowCount;
+ Statements.GetRows = &table.GetRows;
+ Statements.GetItems = &table.GetItems;
+ // TODO
+ // stmts.FilterRowsStatement = ;
+
+ GenericTableView<SaleEntry>::OnProjectChanged(newProject);
+ }
+#pragma clang diagnostic pop
+};
+
+class PurchasesTableView : public GenericTableView<PurchaseEntry>
+{
+public:
+ PurchasesTableView()
+ {
+ mEditDialogTitle = I18N_TEXT("Edit purchase entry", L10N_DATABASE_PURCHASES_VIEW_EDIT_DIALOG_TITLE);
+ }
+
+#pragma clang diagnostic push
+#pragma ide diagnostic ignored "HidingNonVirtualFunction"
+ void OnProjectChanged(Project* newProject)
+ {
+ auto& table = newProject->Database.GetPurchases();
+ Statements.GetRowCount = &table.GetRowCount;
+ Statements.GetRows = &table.GetRows;
+ Statements.GetItems = &table.GetItems;
+ // TODO
+ // stmts.FilterRowsStatement = ;
+
+ GenericTableView<PurchaseEntry>::OnProjectChanged(newProject);
+ }
+#pragma clang diagnostic pop
+};
+} // namespace CPLT_UNITY_ID
+
+void UI::DatabaseViewTab()
+{
+ auto& gs = GlobalStates::GetInstance();
+
+ static Project* currentProject = nullptr;
+ static CPLT_UNITY_ID::SalesTableView sales;
+ static CPLT_UNITY_ID::PurchasesTableView purchases;
+
+ if (currentProject != gs.GetCurrentProject()) {
+ currentProject = gs.GetCurrentProject();
+ sales.OnProjectChanged(currentProject);
+ purchases.OnProjectChanged(currentProject);
+ }
+
+ if (ImGui::BeginTabBar("DatabaseViewTabs")) {
+ if (ImGui::BeginTabItem(I18N_TEXT("Sales", L10N_DATABASE_SALES_VIEW_TAB_NAME))) {
+ sales.Display();
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem(I18N_TEXT("Purchases", L10N_DATABASE_PURCHASES_VIEW_TAB_NAME))) {
+ purchases.Display();
+ ImGui::EndTabItem();
+ }
+ ImGui::EndTabBar();
+ }
+}