#include "ui.hpp" #include "strops.hpp" #include "memops.hpp" #include "config.hpp" #include "locales.hpp" #include "countries.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 InputTextWithErrorHint(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 FileSelect(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 // @localize 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); // @localize } return result; } void CountryDropdown(char* buffer, size_t buf_size, bool activated_only) { const char* selected_country = 0; float widthAvailable = ImGui::GetContentRegionAvail().x; ImGui::SetNextItemWidth(widthAvailable*0.5f); const char* countries[300]; bool enabled_countries[300] = {0}; for (int x = 0; x < country::get_count(); x++) { char locale_str[20]; strops::format(locale_str, 20, "country.%s", country::get_code_by_index(x)); countries[x] = locale::get(locale_str); if (activated_only) { enabled_countries[x] = country::is_enabled(country::get_code_by_index(x)); } else { enabled_countries[x] = 1; } } for (int i = 0; i < country::get_count(); i++) { if (strops::equals(country::get_code_by_index(i), buffer)) { 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 < country::get_count(); n++) { if (enabled_countries[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, country::get_code_by_index(selected_country_index), buf_size); } } void ContactTypeDropdown(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); } } typedef struct { const char* id; bool is_open; } open_autocomplete_ref; int InputTextWithAutocomplete(const char* hint, char* buffer, size_t buf_size, char* suggestions[], int suggestion_count, bool has_error) { int result = -1; const u32 max_ref_count = 10; static open_autocomplete_ref open_refs[max_ref_count] = {0}; open_autocomplete_ref* found_ref = 0; { for (u32 i = 0; i < max_ref_count; i++) { if (open_refs[i].id == buffer) { found_ref = &open_refs[i]; break; } } if (!found_ref) { for (u32 i = 0; i < max_ref_count; i++) { if (open_refs[i].id == 0) { found_ref = &open_refs[i]; found_ref->id = buffer; found_ref->is_open = false; break; } } } if (!found_ref) assert(0); } bool is_open = found_ref->is_open; 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(); } } found_ref->is_open = is_open; return result; } static void _ContactFormNameAutocomplete(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*)memops::alloc(200); strops::format(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::InputTextWithAutocomplete(locale::get("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name), (char**)autocomplete_strings, autocomplete_count, has_error); if (autocomplete_index != -1) { memops::copy(buffer, &autocomplete_list[autocomplete_index], sizeof(contact)); } for (int i = 0; i < autocomplete_count; i++) { memops::unalloc(autocomplete_strings[i]); } } void ContactForm(contact* buffer, bool viewing_only, bool with_autocomplete, bool active_countries_only) { a_err last_err = administration::contact_is_valid(*buffer); ImGui::PushID(buffer); ImGui::Spacing(); ImGui::BeginDisabled(); if (!viewing_only) ImGui::EndDisabled(); if (with_autocomplete) _ContactFormNameAutocomplete(buffer, last_err & A_ERR_MISSING_NAME); else ImGui::InputTextWithErrorHint(locale::get("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name), last_err & A_ERR_MISSING_NAME); ImGui::AddressForm(&buffer->address, last_err, active_countries_only); ImGui::ContactTypeDropdown(&buffer->type); // Fields only required for businesses. if (buffer->type == contact_type::CONTACT_BUSINESS) { ImGui::InputTextWithErrorHint(locale::get("contact.form.taxnumber"), buffer->taxid, IM_ARRAYSIZE(buffer->taxid), last_err & A_ERR_MISSING_TAXID); ImGui::InputTextWithErrorHint(locale::get("contact.form.businessnumber"), buffer->businessid, IM_ARRAYSIZE(buffer->businessid), last_err & A_ERR_MISSING_BUSINESSID); } ImGui::InputTextWithErrorHint(locale::get("contact.form.email"), buffer->email, IM_ARRAYSIZE(buffer->email), last_err & A_ERR_MISSING_EMAIL); ImGui::InputTextWithErrorHint(locale::get("contact.form.phonenumber"), buffer->phone_number, IM_ARRAYSIZE(buffer->phone_number), 0); ImGui::InputTextWithErrorHint(locale::get("contact.form.bankaccount"), buffer->bank_account, IM_ARRAYSIZE(buffer->bank_account), 0); if (viewing_only) ImGui::EndDisabled(); ImGui::PopID(); } void CostCenterDropdown(char* costcenter_id) { u32 costcenter_count = administration::cost_center_count(); cost_center* buffer = (cost_center*) memops::alloc(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 (!strops::empty(costcenter_id)) { for (u32 i = 0; i < costcenter_count; i++) { if (strops::equals(buffer[i].id, costcenter_id)) { 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 && strops::equals(selected_costcenter->id, buffer[n].id); 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); } memops::unalloc(buffer); } void ProjectDropdown(char* project_id) { u32 project_count = administration::project_count(); project* buffer = (project*) memops::alloc(sizeof(project) * project_count); project* selected_project = NULL; project_count = administration::project_get_all(buffer); // Select project by given id. if (!strops::empty(project_id)) { for (u32 i = 0; i < project_count; i++) { if (strops::equals(buffer[i].id, project_id)) { 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 && strops::equals(selected_project->id, buffer[n].id); 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); } memops::unalloc(buffer); } void TaxRateDropdown(char* tax_internal_code, bool outgoing, bool has_error) { u32 tax_rate_count = administration::tax_rate_count(); tax_rate* buffer = (tax_rate*) memops::alloc(sizeof(tax_rate) * tax_rate_count); tax_rate* selected_tax_rate = NULL; tax_rate_count = administration::tax_rate_get_all(buffer, outgoing ? tax_rate_type::TAX_RATE_OUTGOING_INVOICE : tax_rate_type::TAX_RATE_INCOMMING_INVOICE); // Select tax rate by given id. if (!strops::empty(tax_internal_code)) { for (u32 i = 0; i < tax_rate_count; i++) { if (strops::equals(buffer[i].internal_code, tax_internal_code)) { selected_tax_rate = &buffer[i]; break; } } } int selected_tax_rate_index = -1; char rate_str_buf[MAX_LEN_LONG_DESC]; if (selected_tax_rate) { char category_code_desc[MAX_LEN_LONG_DESC]; strops::format(category_code_desc, MAX_LEN_LONG_DESC, "taxrate.code.%s", selected_tax_rate->internal_code); strops::format(rate_str_buf, MAX_LEN_LONG_DESC, "%s", locale::get(category_code_desc)); } 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 && strops::equals(selected_tax_rate->internal_code, buffer[n].internal_code); char category_code_desc[MAX_LEN_LONG_DESC]; strops::format(category_code_desc, MAX_LEN_LONG_DESC, "taxrate.code.%s", buffer[n].internal_code); strops::format(rate_str_buf, MAX_LEN_LONG_DESC, "%s", locale::get(category_code_desc)); 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_internal_code, buffer[selected_tax_rate_index].internal_code, MAX_LEN_SHORT_DESC); } memops::unalloc(buffer); } bool CurrencyDropdown(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 (!strops::empty(currency)) { for (int i = 0; i < currency_count; i++) { if (strops::equals(currencies[i], currency)) { 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 ToggleDropdown(bool *buffer, char* option1, char* option2) { const char* items[] = { option1, option2 }; char ID[MAX_LEN_LONG_DESC]; strops::format(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(); } } bool WarningIcon(float radius) { ImGui::SameLine(); ImVec2 cursor_pos = ImGui::GetCursorScreenPos(); ImVec2 center = ImVec2(cursor_pos.x + radius, cursor_pos.y + radius + 4); ImGui::PushID((int)center.y); ImDrawList* draw_list = ImGui::GetWindowDrawList(); // Draw red circle draw_list->AddCircleFilled(center, radius, IM_COL32(255, 0, 0, 255)); // Draw exclamation mark float line_height = radius * 0.9f; float line_thickness = radius * 0.3f; float dot_radius = radius * 0.2f; ImVec2 line_start(center.x - line_thickness/3.0f, center.y - line_height * 0.7f); ImVec2 line_end(center.x - line_thickness/3.0f, center.y + line_height * 0.1f); // Vertical bar draw_list->AddLine(line_start, line_end, IM_COL32(255, 255, 255, 255), line_thickness); // Bottom dot draw_list->AddCircleFilled(ImVec2(center.x - dot_radius/3.0f, center.y + line_height * 0.55f), dot_radius, IM_COL32(255, 255, 255, 255)); ImGui::SetCursorScreenPos(ImVec2(center.x - radius, center.y - radius)); ImGui::InvisibleButton("##warning_icon", ImVec2(radius * 2, radius * 2)); ImGui::PopID(); if (ImGui::IsItemHovered()) { return true; } return false; } void DeliveryInfoForm(delivery_info* buffer, bool viewing_only) { a_err last_err = administration::addressee_is_valid(*buffer); ImGui::PushID(buffer); ImGui::Spacing(); ImGui::BeginDisabled(); if (!viewing_only) ImGui::EndDisabled(); ImGui::InputTextWithErrorHint(locale::get("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name), last_err & A_ERR_MISSING_NAME); ImGui::AddressForm(&buffer->address, last_err); if (viewing_only) ImGui::EndDisabled(); ImGui::PopID(); } void AddressForm(address* buffer, a_err last_err, bool active_countries_only) { ImGui::InputTextWithErrorHint(locale::get("contact.form.address1"), buffer->address1, IM_ARRAYSIZE(buffer->address1), last_err & A_ERR_MISSING_ADDRESS1); ImGui::InputTextWithErrorHint(locale::get("contact.form.address2"), buffer->address2, IM_ARRAYSIZE(buffer->address2), 0); ImGui::InputTextWithErrorHint(locale::get("contact.form.city"), buffer->city, IM_ARRAYSIZE(buffer->city), last_err & A_ERR_MISSING_CITY); ImGui::InputTextWithErrorHint(locale::get("contact.form.postal"), buffer->postal, IM_ARRAYSIZE(buffer->postal), last_err & A_ERR_MISSING_POSTAL); ImGui::InputTextWithErrorHint(locale::get("contact.form.region"), buffer->region, IM_ARRAYSIZE(buffer->region), 0); ImGui::CountryDropdown(buffer->country_code, IM_ARRAYSIZE(buffer->country_code), active_countries_only); } }