diff options
Diffstat (limited to 'app/source/Cplt/UI/UI_DatabaseView.cpp')
-rw-r--r-- | app/source/Cplt/UI/UI_DatabaseView.cpp | 668 |
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(); + } +} |