#include "UI.hpp" #include "Model/Filter.hpp" #include "Model/Project.hpp" #include "UI/Localization.hpp" #include "UI/States.hpp" #include "Utils/ScopeGuard.hpp" #include "Utils/Time.hpp" #include #include #include #include #include #include #include #include namespace { // TODO move to Settings constexpr int kMaxEntriesPerPage = 20; constexpr int kSummaryItemCount = 3; constexpr int kSummaryMaxLength = 25; enum class DeliveryDirection { FactoryToWarehouse, WarehouseToCustomer, }; struct ItemEntry { int ItemId; int Count; }; struct DeliveryEntry { std::vector 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"; } } }; class GenericTableView { protected: // Translation entries for implementer to fill out const char* mEditDialogTitle; SQLite::Statement* mGetRowCountStatement; SQLite::Statement* mGetRowsStatement; SQLite::Statement* mFilterRowsStatement; Project* mProject; /// Current active filter object, or \c nullptr. std::unique_ptr mActiveFilter; /// Inclusive. /// \see mLastCachedRowId int64_t mFirstCachedRowId; /// Inclusive. /// \see mFirstCachedRowId int64_t mLastCachedRowId; /// A vector of row ids of entries (in \c mEntries) that are visible under the current filter. To use these indices, the elements should be mapped to /// index of the list of entries by adding \c mFirstCachedRowId. /// The list of entries is a cached, contiguous (row id of each entry is monotonically increasing, but not necessarily starts at 0) list /// of ready-to-be-presented entries, held by the implementer. std::vector 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 mCurrentPage; int mSelectedEntryRowId; public: static int CalcPageForRowId(int64_t rowId) { return rowId / 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 CalcRangeForPage(int page) { int begin = page * kMaxEntriesPerPage; return { begin, begin + kMaxEntriesPerPage }; } Project* GetProject() const { return mProject; } virtual void OnProjectChanged(Project* newProject) { mProject = newProject; if (mGetRowCountStatement->executeStep()) { mRowCount = mGetRowCountStatement->getColumn(0).getInt(); } else { std::cerr << "Failed to fetch row count from SQLite.\n"; mRowCount = 0; } mFirstCachedRowId = 0; mLastCachedRowId = 0; ClearEntries(); mActiveEntries.clear(); UpdateLastPage(); SetPage(0); mSelectedEntryRowId = -1; } TableRowsFilter* GetFilter() const { return mActiveFilter.get(); } virtual void OnFilterChanged() { auto& stmt = *mFilterRowsStatement; // 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); mSelectedEntryRowId = -1; } void OnFilterChanged(std::unique_ptr filter) { mActiveFilter = std::move(filter); OnFilterChanged(); } void Draw() { bool dummy = true; auto ls = LocaleStrings::Instance.get(); if (ImGui::Button(ICON_FA_ARROW_LEFT, mCurrentPage == 0)) { mSelectedEntryRowId = -1; 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)) { mSelectedEntryRowId = -1; SetPage(mCurrentPage + 1); } ImGui::SameLine(); if (ImGui::Button(ls->Edit.Get(), mSelectedEntryRowId == -1)) { ImGui::OpenPopup(mEditDialogTitle); } if (ImGui::BeginPopupModal(mEditDialogTitle, &dummy, ImGuiWindowFlags_AlwaysAutoResize)) { EditEntry(mSelectedEntryRowId); ImGui::EndPopup(); } ImGui::SameLine(); if (ImGui::Button(ls->Add.Get())) { // TODO } ImGui::SameLine(); if (ImGui::Button(ls->Delete.Get(), mSelectedEntryRowId == -1)) { // TODO } ImGui::Columns(2); { DrawMainTable(); ImGui::NextColumn(); if (mSelectedEntryRowId == -1) { ImGui::TextWrapped("%s", ls->SelectOrderToShowAssociatedDeliveries.Get()); } else { DrawDeliveriesTable(); } ImGui::NextColumn(); } ImGui::Columns(1); } void DrawMainTable() { if (ImGui::BeginTable("DataTable", GetTableColumnCount(), ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { SetupTableColumns(); ImGui::TableHeadersRow(); auto [begin, end] = CalcRangeForPage(mCurrentPage); if (mActiveFilter) { end = std::min(end, (int64_t)mActiveEntries.size() - 1); for (int i = begin; i < end; ++i) { int rowId = mActiveEntries[i]; DisplayEntry(rowId); } } else { end = std::min(end, mLastCachedRowId); for (int rowId = begin; rowId < end; ++rowId) { DisplayEntry(rowId); } } ImGui::EndTable(); } } void DrawDeliveriesTable() { auto ls = LocaleStrings::Instance.get(); if (ImGui::BeginTable("DeliveriesTable", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { ImGui::TableSetupColumn(ls->DatabaseShipmentTimeColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseArrivalTimeColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseItemsColumn.Get()); ImGui::TableHeadersRow(); auto& deliveries = GetEntryAssociatedDeliveries(mSelectedEntryRowId); 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(); } } void SetPage(int page) { mCurrentPage = page; EnsureCacheCoversPage(page); } int RowIdToIndex(int64_t rowId) const { return rowId - mFirstCachedRowId; } int64_t IndexToRowId(int index) const { return index + mFirstCachedRowId; } std::vector LoadItems(SQLite::Statement& stmt, int64_t rowId) { // clang-format off DEFER { stmt.reset(); }; // clang-format on stmt.bind(1, rowId); std::vector entries; int itemIdCol = stmt.getColumnIndex("ItemId"); int countCol = stmt.getColumnIndex("Count"); while (stmt.executeStep()) { entries.push_back(ItemEntry{ .ItemId = stmt.getColumn(itemIdCol).getInt(), .Count = stmt.getColumn(countCol).getInt(), }); } return entries; } std::string CreateItemsSummary(const std::vector& items) { if (items.empty()) { return ""; } 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 LoadDeliveriesEntries(int64_t orderRowId, DeliveryDirection type) { bool outgoingFlag; switch (type) { case DeliveryDirection::FactoryToWarehouse: outgoingFlag = false; break; case DeliveryDirection::WarehouseToCustomer: outgoingFlag = true; break; } auto& stmt = mProject->GetTransactionsModel().GetDeliveries().FilterByTypeAndId; // clang-format off DEFER { stmt.reset(); }; // clang-format on stmt.bind(1, orderRowId); stmt.bind(2, outgoingFlag); std::vector entries; int rowIdCol = stmt.getColumnIndex("Id"); int sendTimeCol = stmt.getColumnIndex("ShipmentTime"); int arrivalTimeCol = stmt.getColumnIndex("ArrivalTime"); while (stmt.executeStep()) { auto items = LoadItems( mProject->GetTransactionsModel().GetDeliveries().GetItems, stmt.getColumn(rowIdCol).getInt64()); auto summary = CreateItemsSummary(items); entries.push_back(DeliveryEntry{ .Items = std::move(items), .ItemsSummary = std::move(summary), .ShipmentTime = StringifyTimeStamp(stmt.getColumn(arrivalTimeCol).getInt64()), .ArriveTime = StringifyTimeStamp(stmt.getColumn(sendTimeCol).getInt64()), .Direction = type, }); } return entries; } protected: virtual int GetTableColumnCount() const = 0; virtual void SetupTableColumns() = 0; virtual const std::vector& GetEntryAssociatedDeliveries(int rowId) = 0; virtual void DisplayEntry(int rowId) = 0; virtual void EditEntry(int rowId) = 0; virtual void ClearEntries() = 0; void DrawItems(const std::vector& items) { for (auto& item : items) { auto& name = mProject->Products.Find(item.ItemId)->GetName(); ImGui::Text("%s × %d", name.c_str(), item.Count); } } 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) + 1) * kMaxEntriesPerPage; doRebuild = true; } if (lastRow > mLastCachedRowId) { newLast = (CalcPageForRowId(lastRow) + 1) * kMaxEntriesPerPage; doRebuild = true; } if (!doRebuild) return; EnsureCacheCoversImpl(newFirst, newLast); } /// To be implemented by child classes, presumable calling LoadRange() to get the front and back new contents. /// \param newFirst The first rowid the new cache should cover /// \param newLast The last rowid the new cache should cover virtual void EnsureCacheCoversImpl(int newFirst, int newLast) = 0; template void LoadExtraEntries(std::vector& entries, int newFirst, int newLast, TCollector&& collector) { auto front = LoadRange(newFirst, mFirstCachedRowId, collector); auto back = LoadRange(mLastCachedRowId + 1, newLast + 1, collector); mFirstCachedRowId -= front.size(); mLastCachedRowId += back.size(); entries.insert(entries.begin(), std::make_move_iterator(front.begin()), std::make_move_iterator(front.end())); entries.insert(entries.end(), std::make_move_iterator(back.begin()), std::make_move_iterator(back.end())); } template std::vector LoadRange(int64_t begin, int64_t end, TCollector&& collector) { std::vector result; size_t size = end - begin; if (size == 0) { return result; } result.reserve(size); // clang-format off DEFER { mGetRowsStatement->reset(); }; // clang-format on mGetRowsStatement->bind(1, begin); mGetRowsStatement->bind(2, end); collector(result); return result; } private: void UpdateLastPage() { mLastPage = mActiveEntries.empty() ? CalcPageForRowId(mRowCount) : CalcPageForRowId(mActiveEntries.back()); } }; struct SaleEntry { std::vector AssociatedDeliveries; std::vector Items; std::string ItemsSummary; std::string Customer; std::string Deadline; std::string DeliveryTime; bool DeliveriesCached = false; }; class SalesTableView : public GenericTableView { private: /// 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 mEntries; public: SalesTableView() { auto ls = LocaleStrings::Instance.get(); mEditDialogTitle = ls->EditSaleEntryDialogTitle.Get(); } virtual void OnProjectChanged(Project* newProject) override { auto& sales = newProject->GetTransactionsModel().GetSales(); mGetRowCountStatement = &sales.GetRowCount; mGetRowsStatement = &sales.GetRows; // mFilterRowsStatement = &sales.FilterRows; GenericTableView::OnProjectChanged(newProject); } protected: virtual int GetTableColumnCount() const override { return 4; } virtual void SetupTableColumns() override { auto ls = LocaleStrings::Instance.get(); ImGui::TableSetupColumn(ls->DatabaseCustomerColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseDeadlineColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseCompletionTimeColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseItemsColumn.Get()); } virtual const std::vector& GetEntryAssociatedDeliveries(int rowId) override { auto& entry = mEntries[RowIdToIndex(rowId)]; if (!entry.DeliveriesCached) { entry.AssociatedDeliveries = LoadDeliveriesEntries(rowId, DeliveryDirection::FactoryToWarehouse); entry.DeliveriesCached = true; } return entry.AssociatedDeliveries; } virtual void DisplayEntry(int rowId) override { auto& entry = mEntries[RowIdToIndex(rowId)]; auto ls = LocaleStrings::Instance.get(); ImGui::PushID(rowId); ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::Selectable(entry.Customer.c_str(), mSelectedEntryRowId == rowId, ImGuiSelectableFlags_SpanAllColumns)) { mSelectedEntryRowId = rowId; } ImGui::TableNextColumn(); ImGui::TextUnformatted(entry.Deadline.c_str()); ImGui::TableNextColumn(); if (entry.DeliveryTime.empty()) { ImGui::TextUnformatted(ls->NotDelivered.Get()); } else { ImGui::TextUnformatted(entry.DeliveryTime.c_str()); } ImGui::TableNextColumn(); if (ImGui::TreeNode(entry.ItemsSummary.c_str())) { DrawItems(entry.Items); ImGui::TreePop(); } ImGui::PopID(); } virtual void EditEntry(int rowId) override { // TODO } virtual void ClearEntries() override { mEntries.clear(); } virtual void EnsureCacheCoversImpl(int newFirst, int newLast) override { auto CollectRows = [&](std::vector& result) { auto& stmt = *mGetRowsStatement; int rowIdCol = stmt.getColumnIndex("Id"); int customerCol = stmt.getColumnIndex("Customer"); int deadlineCol = stmt.getColumnIndex("Deadline"); int deliveryTimeCol = stmt.getColumnIndex("DeliveryTime"); while (stmt.executeStep()) { auto customerId = stmt.getColumn(customerCol).getInt(); auto items = LoadItems( mProject->GetTransactionsModel().GetSales().GetItems, stmt.getColumn(rowIdCol).getInt64()); auto itemsSummary = CreateItemsSummary(items); result.push_back(SaleEntry{ .Items = std::move(items), .ItemsSummary = std::move(itemsSummary), .Customer = mProject->Customers.Find(customerId)->GetName(), .Deadline = StringifyTimeStamp(stmt.getColumn(deadlineCol).getInt64()), .DeliveryTime = StringifyTimeStamp(stmt.getColumn(deliveryTimeCol).getInt64()), }); } }; LoadExtraEntries(mEntries, newFirst, newLast, CollectRows); } }; struct PurchaseEntry { std::vector AssociatedDeliveries; std::vector Items; std::string ItemsSummary; std::string Factory; std::string OrderTime; std::string DeliveryTime; bool DeliveriesCached; }; class PurchasesTableView : public GenericTableView { private: std::vector mEntries; public: PurchasesTableView() { auto ls = LocaleStrings::Instance.get(); mEditDialogTitle = ls->EditPurchaseEntryDialogTitle.Get(); } virtual void OnProjectChanged(Project* newProject) override { auto& purchases = newProject->GetTransactionsModel().GetPurchases(); mGetRowCountStatement = &purchases.GetRowCount; mGetRowsStatement = &purchases.GetRows; // mFilterRowsStatement = &purchases.FilterRowsStatement; GenericTableView::OnProjectChanged(newProject); } protected: virtual int GetTableColumnCount() const override { return 4; } virtual void SetupTableColumns() override { auto ls = LocaleStrings::Instance.get(); ImGui::TableSetupColumn(ls->DatabaseFactoryColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseOrderTimeColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseCompletionTimeColumn.Get()); ImGui::TableSetupColumn(ls->DatabaseItemsColumn.Get()); } virtual const std::vector& GetEntryAssociatedDeliveries(int rowId) override { auto& entry = mEntries[RowIdToIndex(rowId)]; if (!entry.DeliveriesCached) { entry.AssociatedDeliveries = LoadDeliveriesEntries(rowId, DeliveryDirection::FactoryToWarehouse); entry.DeliveriesCached = true; } return entry.AssociatedDeliveries; } virtual void DisplayEntry(int rowId) override { auto& entry = mEntries[RowIdToIndex(rowId)]; auto ls = LocaleStrings::Instance.get(); ImGui::PushID(rowId); ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::Selectable(entry.Factory.c_str(), mSelectedEntryRowId == rowId, ImGuiSelectableFlags_SpanAllColumns)) { mSelectedEntryRowId = rowId; } ImGui::TableNextColumn(); if (entry.OrderTime.empty()) { ImGui::TextUnformatted(ls->NotDelivered.Get()); } else { ImGui::TextUnformatted(entry.OrderTime.c_str()); } ImGui::TableNextColumn(); if (entry.DeliveryTime.empty()) { ImGui::TextUnformatted(ls->NotDelivered.Get()); } else { ImGui::TextUnformatted(entry.DeliveryTime.c_str()); } ImGui::TableNextColumn(); if (ImGui::TreeNode(entry.ItemsSummary.c_str())) { DrawItems(entry.Items); ImGui::TreePop(); } ImGui::PopID(); } virtual void EditEntry(int rowId) override { // TODO } virtual void ClearEntries() override { mEntries.clear(); } virtual void EnsureCacheCoversImpl(int newFirst, int newLast) override { auto CollectRows = [&](std::vector& result) { auto& stmt = *mGetRowsStatement; int rowIdCol = stmt.getColumnIndex("Id"); int factoryCol = stmt.getColumnIndex("Factory"); int orderTimeCol = stmt.getColumnIndex("OrderTime"); int deliveryTimeCol = stmt.getColumnIndex("DeliveryTime"); while (stmt.executeStep()) { auto factoryId = stmt.getColumn(factoryCol).getInt(); auto items = LoadItems( mProject->GetTransactionsModel().GetPurchases().GetItems, stmt.getColumn(rowIdCol).getInt64()); auto itemsSummary = CreateItemsSummary(items); result.push_back(PurchaseEntry{ .Items = std::move(items), .ItemsSummary = std::move(itemsSummary), .Factory = mProject->Factories.Find(factoryId)->GetName(), .OrderTime = StringifyTimeStamp(stmt.getColumn(orderTimeCol).getInt64()), .DeliveryTime = StringifyTimeStamp(stmt.getColumn(deliveryTimeCol).getInt64()), }); } }; LoadExtraEntries(mEntries, newFirst, newLast, CollectRows); } }; } // namespace void UI::DatabaseViewTab() { auto ls = LocaleStrings::Instance.get(); auto& uis = UIState::GetInstance(); static Project* currentProject = nullptr; static SalesTableView sales; static PurchasesTableView purchases; if (currentProject != uis.CurrentProject.get()) { currentProject = uis.CurrentProject.get(); sales.OnProjectChanged(currentProject); purchases.OnProjectChanged(currentProject); } if (ImGui::BeginTabBar("##DatabaseViewTabs")) { if (ImGui::BeginTabItem(ls->SalesViewTab.Get())) { sales.Draw(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(ls->PurchasesViewTab.Get())) { purchases.Draw(); ImGui::EndTabItem(); } ImGui::EndTabBar(); } }