#include "UI.hpp" #include "Model/Filter.hpp" #include "Model/GlobalStates.hpp" #include "Model/Project.hpp" #include "UI/Localization.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 = 32; constexpr int kSummaryItemCount = 3; constexpr int kSummaryMaxLength = 25; std::pair 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 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 AssociatedDeliveries; std::vector 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 AssociatedDeliveries; std::vector Items; std::string ItemsSummary; std::string Factory; std::string OrderTime; std::string DeliveryTime; int Id; bool DeliveriesCached; }; template 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; 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 mActiveFilter; tsl::robin_map mPages; /// A vector of entry indices (in \c mEntries) that are visible under the current filter. 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 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 filter) { mActiveFilter = std::move(filter); OnFilterChanged(); } void Display() { bool dummy = true; auto ls = LocaleStrings::Instance.get(); 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(ls->Edit.Get(), 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(ls->Add.Get())) { // TODO } ImGui::SameLine(); if (ImGui::Button(ls->Delete.Get(), mSelectRow == -1)) { // TODO } ImGui::Columns(2); { DisplayMainTable(); ImGui::NextColumn(); if (mSelectRow == -1) { ImGui::TextWrapped("%s", ls->SelectOrderToShowAssociatedDeliveries.Get()); } 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 CalcRangeForPage(int page) { int begin = page * kMaxEntriesPerPage; return { begin, begin + kMaxEntriesPerPage }; } void DisplayMainTable() { auto ls = LocaleStrings::Instance.get(); if (ImGui::BeginTable("DataTable", kColumnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_ScrollX)) { if constexpr (kHasCustomer) ImGui::TableSetupColumn(ls->DatabaseCustomerColumn.Get()); if constexpr (kHasDeadline) ImGui::TableSetupColumn(ls->DatabaseDeadlineColumn.Get()); if constexpr (kHasFactory) ImGui::TableSetupColumn(ls->DatabaseFactoryColumn.Get()); if constexpr (kHasOrderTime) ImGui::TableSetupColumn(ls->DatabaseOrderTimeColumn.Get()); if constexpr (kHasCompletionTime) ImGui::TableSetupColumn(ls->DatabaseCompletionTimeColumn.Get()); if constexpr (kHasItems) ImGui::TableSetupColumn(ls->DatabaseItemsColumn.Get()); 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) { auto ls = LocaleStrings::Instance.get(); 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(ls->NotDelivered.Get()); } 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() { 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& 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 LoadItems(SQLite::Statement& stmt, int64_t id) { // clang-format off DEFER { stmt.reset(); }; // clang-format on stmt.bind(1, id); std::vector 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& 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 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 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& 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 { public: SalesTableView() { auto ls = LocaleStrings::Instance.get(); mEditDialogTitle = ls->EditSaleEntryDialogTitle.Get(); } #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::OnProjectChanged(newProject); } #pragma clang diagnostic pop }; class PurchasesTableView : public GenericTableView { public: PurchasesTableView() { auto ls = LocaleStrings::Instance.get(); mEditDialogTitle = ls->EditPurchaseEntryDialogTitle.Get(); } #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::OnProjectChanged(newProject); } #pragma clang diagnostic pop }; } // namespace void UI::DatabaseViewTab() { auto ls = LocaleStrings::Instance.get(); auto& gs = GlobalStates::GetInstance(); static Project* currentProject = nullptr; static SalesTableView sales; static PurchasesTableView purchases; if (currentProject != gs.GetCurrentProject()) { currentProject = gs.GetCurrentProject(); sales.OnProjectChanged(currentProject); purchases.OnProjectChanged(currentProject); } if (ImGui::BeginTabBar("DatabaseViewTabs")) { if (ImGui::BeginTabItem(ls->SalesViewTab.Get())) { sales.Display(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(ls->PurchasesViewTab.Get())) { purchases.Display(); ImGui::EndTabItem(); } ImGui::EndTabBar(); } }