#include #include #include "ui.hpp" #include "strops.hpp" #include "config.hpp" #include "locales.hpp" #include "administration.hpp" #include "tinyfiledialogs.h" namespace ImGui { void InputTextWithError(const char* text, char* buffer, size_t buf_size, bool has_error) { if (has_error) { ImGui::PushStyleColor(ImGuiCol_Border, config::colors::COLOR_ERROR_OUTLINE); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); } ImGui::InputText(text, buffer, buf_size); if (has_error) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); } } void FormInputTextWithErrorHint(const char* hint, char* buffer, size_t buf_size, bool has_error) { float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); if (has_error) { ImGui::PushStyleColor(ImGuiCol_Border, config::colors::COLOR_ERROR_OUTLINE); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); } ImGui::InputTextWithHint(hint, hint, buffer, buf_size); if (has_error) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x); ImGui::TextColored(ImVec4(1, 0, 0, 1), "*"); } } bool FormInvoiceFileSelector(char* text, char* buffer) { bool result = false; float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); if (ImGui::Button(text)) { const char *filterPatterns[] = { "*.pdf" }; const char *file = tinyfd_openFileDialog( "Choose a file", // dialog title // @locale::get NULL, // default path 1, // number of filter patterns filterPatterns, // filter patterns array NULL, // single filter description (can be NULL) 0); // allowMultiple (0 = single) if (file) { strops::copy(buffer, file, MAX_LEN_PATH); buffer[MAX_LEN_PATH-1] = '\0'; result = true; } } if (buffer[0] != '\0') { ImGui::SameLine(); if (ImGui::Button("Clear")) { buffer[0] = '\0'; result = true; } ImGui::SameLine(); ImGui::TextWrapped("Selected: %s", buffer); // @locale::get } return result; } void FormCountryCombo(char* buffer, size_t buf_size) { const char* selected_country = 0; float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); const char* countries[30]; for (int x = 0; x < config::country_count; x++) { char locale_str[20]; snprintf(locale_str, 20, "country.%s", config::country_codes[x]); countries[x] = locale::get(locale_str); } for (int i = 0; i < config::country_count; i++) { if (strcmp(config::country_codes[i], buffer) == 0) { selected_country = countries[i]; break; } } bool has_a_selection = selected_country != 0; int selected_country_index = -1; if (!has_a_selection) { ImGui::PushStyleColor(ImGuiCol_Border, config::colors::COLOR_ERROR_OUTLINE); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); } if (ImGui::BeginCombo(locale::get("contact.form.country"), selected_country)) { for (int n = 0; n < config::country_count; n++) { bool is_selected = (selected_country == countries[n]); if (ImGui::Selectable(countries[n], is_selected)) { selected_country = countries[n]; selected_country_index = n; } } ImGui::EndCombo(); } if (!has_a_selection) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x); ImGui::TextColored(ImVec4(1, 0, 0, 1), "*"); } if (selected_country_index != -1) { strops::copy(buffer, config::country_codes[selected_country_index], buf_size); } } void FormContactTypeCombo(contact_type* type) { float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); const char* customer_types[2] = { locale::get("contact.form.type.business"), locale::get("contact.form.type.consumer") }; int currentItem = static_cast(*type); if (ImGui::Combo(locale::get("contact.form.type"), ¤tItem, customer_types, IM_ARRAYSIZE(customer_types))) { *type = static_cast(currentItem); } } int TextInputWithAutocomplete(const char* hint, char* buffer, size_t buf_size, char* suggestions[], int suggestion_count, bool has_error) { int result = -1; static bool is_open = false; if (has_error) { ImGui::PushStyleColor(ImGuiCol_Border, config::colors::COLOR_ERROR_OUTLINE); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); } ImGui::InputTextWithHint(hint, hint, buffer, buf_size); if (has_error) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); ImGui::SameLine(); ImGui::SetCursorPosX(ImGui::GetCursorPosX() - ImGui::CalcTextSize("*").x); ImGui::TextColored(ImVec4(1, 0, 0, 1), "*"); } if (buffer[0] == '\0' && is_open) is_open = false; if (suggestion_count == 0 && is_open) is_open = false; bool is_active = ImGui::IsItemActive(); if (is_active && buffer[0] != '\0' && suggestion_count > 0) { is_open = true; } if (is_open) { ImGui::BeginChild("autocomplete_popup", ImVec2(0, 10.0f + suggestion_count*23.0f), true); { ImVec2 win_pos = ImGui::GetWindowPos(); ImVec2 win_size = ImGui::GetWindowSize(); ImVec2 mouse_pos = ImGui::GetMousePos(); bool mouse_clicked_outside = !is_active && ImGui::IsMouseClicked(0) && (mouse_pos.x < win_pos.x || mouse_pos.x > win_pos.x + win_size.x || mouse_pos.y < win_pos.y || mouse_pos.y > win_pos.y + win_size.y); if (mouse_clicked_outside) is_open = false; for (int i = 0; i < suggestion_count; ++i) { ImGui::PushID(i); if (ImGui::Selectable(suggestions[i])) { // Copy selected suggestion to buffer strops::copy(buffer, suggestions[i], buf_size); buffer[buf_size - 1] = '\0'; result = i; is_open = false; } ImGui::PopID(); } ImGui::EndChild(); } } return result; } void FormContactAutocomplete(contact* buffer, bool has_error) { float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); contact autocomplete_list[5]; int autocomplete_count = administration::contact_get_autocompletions(autocomplete_list, 5, buffer->name); char* autocomplete_strings[5]; for (int i = 0; i < autocomplete_count; i++) { autocomplete_strings[i] = (char*)malloc(200); snprintf(autocomplete_strings[i], 200, "%s (%s %s)", autocomplete_list[i].name, autocomplete_list[i].address.address1, autocomplete_list[i].address.address2); } int autocomplete_index = ImGui::TextInputWithAutocomplete(locale::get("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name), (char**)autocomplete_strings, autocomplete_count, has_error); if (autocomplete_index != -1) { memcpy(buffer, &autocomplete_list[autocomplete_index], sizeof(contact)); } for (int i = 0; i < autocomplete_count; i++) { free(autocomplete_strings[i]); } } void FormCostCenterCombo(char* costcenter_id) { u32 costcenter_count = administration::cost_center_count(); cost_center* buffer = (cost_center*) malloc(sizeof(cost_center) * costcenter_count); cost_center* selected_costcenter = NULL; costcenter_count = administration::cost_center_get_all(buffer); // Select cost center by given id. if (strlen(costcenter_id) > 0) { for (u32 i = 0; i < costcenter_count; i++) { if (strcmp(buffer[i].id, costcenter_id) == 0) { selected_costcenter = &buffer[i]; break; } } } int selected_costcenter_index = -1; if (ImGui::BeginCombo(locale::get("invoice.form.costcenter"), selected_costcenter == NULL ? NULL : locale::get(selected_costcenter->description))) { for (u32 n = 0; n < costcenter_count; n++) { bool is_selected = selected_costcenter && strcmp(selected_costcenter->id, buffer[n].id) == 0; if (ImGui::Selectable(locale::get(buffer[n].description), is_selected)) { selected_costcenter_index = n; } } ImGui::EndCombo(); } if (selected_costcenter_index != -1) { strops::copy(costcenter_id, buffer[selected_costcenter_index].id, MAX_LEN_ID); } free(buffer); } void FormProjectCombo(char* project_id) { u32 project_count = administration::project_count(); project* buffer = (project*) malloc(sizeof(project) * project_count); project* selected_project = NULL; project_count = administration::project_get_all(buffer); // Select project by given id. if (strlen(project_id) > 0) { for (u32 i = 0; i < project_count; i++) { if (strcmp(buffer[i].id, project_id) == 0) { selected_project = &buffer[i]; break; } } } int selected_project_index = -1; if (ImGui::BeginCombo(locale::get("invoice.form.project"), selected_project == NULL ? NULL : selected_project->description)) { for (u32 n = 0; n < project_count; n++) { bool is_selected = selected_project && strcmp(selected_project->id, buffer[n].id) == 0; if (ImGui::Selectable(buffer[n].description, is_selected)) { selected_project_index = n; } } ImGui::EndCombo(); } if (selected_project_index != -1) { strops::copy(project_id, buffer[selected_project_index].id, MAX_LEN_ID); } free(buffer); } void FormTaxRateCombo(char* tax_rate_id, char* orig_country, char* dest_country, bool has_error) { u32 tax_rate_count = administration::tax_rate_count(); tax_rate* buffer = (tax_rate*) malloc(sizeof(tax_rate) * tax_rate_count); tax_rate* selected_tax_rate = NULL; char* tax_country_codes[2] = {orig_country, dest_country}; tax_rate_count = administration::tax_rate_get_by_country(buffer, strcmp(orig_country, dest_country) == 0 ? 1 : 2, tax_country_codes); // Select tax rate by given id. if (strlen(tax_rate_id) > 0) { for (u32 i = 0; i < tax_rate_count; i++) { if (strcmp(buffer[i].id, tax_rate_id) == 0) { selected_tax_rate = &buffer[i]; break; } } } int selected_tax_rate_index = -1; char rate_str_buf[40]; rate_str_buf[0] = 0; if (selected_tax_rate) { if (strcmp(selected_tax_rate->country_code, "00") == 0) { char category_code_desc[MAX_LEN_LONG_DESC]; snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", selected_tax_rate->category_code); snprintf(rate_str_buf, 40, "%s", locale::get(category_code_desc)); } else snprintf(rate_str_buf, 40, "%s/%.1f%%", selected_tax_rate->country_code, selected_tax_rate->rate); } if (has_error) { ImGui::PushStyleColor(ImGuiCol_Border, config::colors::COLOR_ERROR_OUTLINE); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.5f); } if (ImGui::BeginCombo("##Tax Bracket", rate_str_buf)) { for (u32 n = 0; n < tax_rate_count; n++) { bool is_selected = selected_tax_rate && strcmp(selected_tax_rate->id, buffer[n].id) == 0; if (strcmp(buffer[n].country_code, "00") == 0) { char category_code_desc[MAX_LEN_LONG_DESC]; snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", buffer[n].category_code); snprintf(rate_str_buf, 40, "%s", locale::get(category_code_desc)); } else snprintf(rate_str_buf, 40, "%s/%.1f%%", buffer[n].country_code, buffer[n].rate); if (ImGui::Selectable(rate_str_buf, is_selected)) { selected_tax_rate_index = n; } } ImGui::EndCombo(); } if (has_error) { ImGui::PopStyleVar(); ImGui::PopStyleColor(); } if (selected_tax_rate_index != -1) { strops::copy(tax_rate_id, buffer[selected_tax_rate_index].id, MAX_LEN_ID); } free(buffer); } bool FormCurrencyCombo(char* currency) { int currentCurrency = 0; bool result = false; // Top 15 most traded currencies + all EU official currencies const char* currencies[] = { // Top 15 "EUR", "USD", "JPY", "GBP", "AUD", "CAD", "CHF", "CNY", "HKD", "NZD", "SEK", "KRW", "SGD", "NOK", "MXN", // Additional EU currencies "BGN", // Bulgarian Lev "CZK", // Czech Koruna "DKK", // Danish Krone "HUF", // Hungarian Forint "PLN", // Polish Zloty "RON", // Romanian Leu // "HRK", // Croatian Kuna (legacy, replaced by EUR in 2023) }; int currency_count = sizeof(currencies) / sizeof(char*); if (strlen(currency) > 0) { for (int i = 0; i < currency_count; i++) { if (strcmp(currencies[i], currency) == 0) { currentCurrency = i; break; } } } ImGui::SetNextItemWidth(100.0f); if (ImGui::BeginCombo("##currency", currencies[currentCurrency])) { for (int n = 0; n < IM_ARRAYSIZE(currencies); n++) { bool isSelected = (currentCurrency == n); if (ImGui::Selectable(currencies[n], isSelected)) { result = true; strops::copy(currency, currencies[n], MAX_LEN_CURRENCY); } if (isSelected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } return result; } void FormToggleCombo(bool *buffer, char* option1, char* option2) { const char* items[] = { option1, option2 }; char ID[MAX_LEN_LONG_DESC]; snprintf(ID, MAX_LEN_LONG_DESC, "Mode##%p", buffer); if (ImGui::BeginCombo(ID, items[*buffer])) { for (int n = 0; n < 2; n++) { bool is_selected = (n == (int)*buffer); if (ImGui::Selectable(items[n], is_selected)) { *buffer = n; } if (is_selected) { ImGui::SetItemDefaultFocus(); } } ImGui::EndCombo(); } } }