/* * Copyright (c) 2025 Aldrik Ramaekers * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include "strops.hpp" #include "ui.hpp" #include "imgui.h" #include "administration.hpp" #include "locales.hpp" #include "administration_writer.hpp" extern void draw_contact_form(contact* buffer, bool viewing_only = false); static contact company_info; u32 tax_rate_count; tax_rate* tax_rates = 0; u32 cost_center_count; cost_center* cost_centers = 0; static int select_company_tab = 0; static ai_service new_service; void ui_destroy_settings() { free(tax_rates); free(cost_centers); } void ui_setup_settings() { select_company_tab = 1; company_info = administration_company_info_get(); tax_rate_count = administration_tax_rate_count(); tax_rates = (tax_rate*)malloc(tax_rate_count * sizeof(tax_rate)); administration_tax_rate_get_all(tax_rates); cost_center_count = administration_cost_center_count(); cost_centers = (cost_center*)malloc(cost_center_count * sizeof(cost_center)); administration_cost_center_get_all(cost_centers); new_service = administration_get_ai_service(); } static void ui_draw_vat_rates() { static bool is_adding_item = false; static tax_rate new_tax_rate; static bool is_editing_item = false; static u32 editing_item_index = 0; if (ImGui::BeginTable("TableVatRates", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn(localize("settings.vat.table.country"), ImGuiTableColumnFlags_WidthFixed, 220); ImGui::TableSetupColumn(localize("settings.vat.table.rates")); // Used to generate headers for each individual country. char prev_country[MAX_LEN_COUNTRY_CODE]; prev_country[0] = 0; for (u32 i = 0; i < tax_rate_count; i++) { tax_rate c = tax_rates[i]; // Set to false for shared rates. bool can_be_modified = false; // Check for fixed rates shared accross countries. if (strcmp(c.country_code, "00") == 0) { strops_copy(prev_country, c.country_code, 3); can_be_modified = false; } // Generate headers per country. else if (strcmp(c.country_code, prev_country) != 0) { strops_copy(prev_country, c.country_code, 3); // Empty row. ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text(""); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, IM_COL32(69, 69, 69, 255)); ImGui::TableSetColumnIndex(1); ImGui::Text(""); ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, IM_COL32(69, 69, 69, 255)); ImGui::TableNextRow(); char locale_buf[20]; snprintf(locale_buf, sizeof(locale_buf), "country.%s", c.country_code); ImGui::TableSetColumnIndex(0); ImGui::Text(localize(locale_buf)); // If not adding an item already, show + button next to country name. if (!is_adding_item) { ImGui::SameLine(); char btn_name[20]; snprintf(btn_name, sizeof(btn_name), "+##%d",i); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0,0)); if (ImGui::Button(btn_name, ImVec2(20,20))) { is_adding_item = true; is_editing_item = false; new_tax_rate = administration_tax_rate_create_empty(); strops_copy(new_tax_rate.country_code, c.country_code, 3); } ImGui::PopStyleVar(); } ImGui::TableSetColumnIndex(1); ImGui::Text(""); } // Column 1: description of tax rate. Is only displayed on shared tax rates for clarity. ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); char category_code_desc[MAX_LEN_LONG_DESC]; snprintf(category_code_desc, MAX_LEN_LONG_DESC, "taxcategory.%s", c.category_code); ImGui::Text(can_be_modified ? "" : localize(category_code_desc)); // Column 2: When editing, show input for new rate. Else we display the stored rate and check for modify request. ImGui::TableSetColumnIndex(1); if (is_editing_item && editing_item_index == i) { ImGui::InputFloat("##Rate", &new_tax_rate.rate, 1.0f, 5.0f, "%.2f"); if (new_tax_rate.rate < 0.0f) new_tax_rate.rate = 0.0f; if (new_tax_rate.rate > 100.0f) new_tax_rate.rate = 100.0f; ImGui::SameLine(); if (ImGui::Button(localize("form.save"))) { is_editing_item = false; is_adding_item = false; administration_tax_rate_update(new_tax_rate); ui_destroy_settings(); ui_setup_settings(); } ImGui::SameLine(); if (ImGui::Button(localize("form.cancel"))) { is_editing_item = false; is_adding_item = false; memset(&new_tax_rate, 0, sizeof(new_tax_rate)); } } else { ImGui::Text("%.2f%%", c.rate); if (can_be_modified && ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { is_editing_item = true; is_adding_item = false; editing_item_index = i; new_tax_rate = c; } } // When adding a new entry it is displayed at the bottom of the list of the country we are adding to. // Check for end of list (for last country in the list), or check if next country differs from current country. // If it is different we have reached the end of the list for the current country. if (i == tax_rate_count-1 || (i < tax_rate_count-1 && strcmp(tax_rates[i+1].country_code, c.country_code) != 0)) { if (is_adding_item && strcmp(new_tax_rate.country_code, prev_country) == 0) { ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text(""); ImGui::TableSetColumnIndex(1); ImGui::InputFloat("##Rate", &new_tax_rate.rate, 1.0f, 5.0f, "%.2f"); if (new_tax_rate.rate < 0.0f) new_tax_rate.rate = 0.0f; if (new_tax_rate.rate > 100.0f) new_tax_rate.rate = 100.0f; ImGui::SameLine(); if (ImGui::Button(localize("form.save"))) { is_editing_item = false; is_adding_item = false; administration_tax_rate_add(new_tax_rate); ui_destroy_settings(); ui_setup_settings(); } ImGui::SameLine(); if (ImGui::Button(localize("form.cancel"))) { is_editing_item = false; is_adding_item = false; memset(&new_tax_rate, 0, sizeof(new_tax_rate)); } } } } ImGui::EndTable(); } } static void ui_draw_cost_centers() { static bool is_adding_item = false; static cost_center new_cost_center; static bool is_editing_item = false; static u32 editing_item_index = 0; if (ImGui::BeginTable("TableCostCenters", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn(localize("settings.costcenters.table.code"), ImGuiTableColumnFlags_WidthFixed, 140); ImGui::TableSetupColumn(localize("settings.costcenters.table.description")); for (u32 i = 0; i < cost_center_count; i++) { cost_center c = cost_centers[i]; ImGui::TableNextRow(); // Column 1: Code of cost center. ImGui::TableSetColumnIndex(0); ImGui::Text(c.code); // Column 2: When editing, show inputs for new description. Else show stored description and check for modify request. ImGui::TableSetColumnIndex(1); if (is_editing_item && editing_item_index == i) { bool is_desc_valid = !(administration_cost_center_is_valid(new_cost_center) & A_ERR_MISSING_DESCRIPTION); if (!is_desc_valid) ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(105, 43, 43, 255)); ImGui::InputText("##Description", new_cost_center.description, IM_ARRAYSIZE(new_cost_center.description)); if (!is_desc_valid) ImGui::PopStyleColor(); if (!is_desc_valid) ImGui::BeginDisabled(); ImGui::SameLine(); if (ImGui::Button(localize("form.save"))) { is_editing_item = false; is_adding_item = false; administration_cost_center_update(new_cost_center); memset(&new_cost_center, 0, sizeof(new_cost_center)); ui_destroy_settings(); ui_setup_settings(); } if (!is_desc_valid) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(localize("form.cancel"))) { is_editing_item = false; is_adding_item = false; memset(&new_cost_center, 0, sizeof(new_cost_center)); } } else { ImGui::Text(localize(c.description)); if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { is_editing_item = true; is_adding_item = false; editing_item_index = i; new_cost_center = c; } } } // When adding a new item. Show inputs for code and description, check validity, and handle save/cancel. // Form for new entry is displayed at bottom of list. if (is_adding_item) { ImGui::TableNextRow(); bool is_code_valid = !(administration_cost_center_is_valid(new_cost_center) & (A_ERR_CODE_EXISTS|A_ERR_MISSING_CODE)); if (!is_code_valid) ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(105, 43, 43, 255)); ImGui::TableSetColumnIndex(0); ImGui::InputText("##Code", new_cost_center.code, IM_ARRAYSIZE(new_cost_center.code), ImGuiInputTextFlags_CharsUppercase|ImGuiInputTextFlags_CharsNoBlank); if (!is_code_valid) ImGui::PopStyleColor(); bool is_desc_valid = !(administration_cost_center_is_valid(new_cost_center) & A_ERR_MISSING_DESCRIPTION); if (!is_desc_valid) ImGui::PushStyleColor(ImGuiCol_FrameBg, IM_COL32(105, 43, 43, 255)); ImGui::TableSetColumnIndex(1); ImGui::InputText("##Description", new_cost_center.description, IM_ARRAYSIZE(new_cost_center.description)); if (!is_desc_valid) ImGui::PopStyleColor(); bool can_save = is_code_valid && is_desc_valid; if (!can_save) ImGui::BeginDisabled(); ImGui::SameLine(); if (ImGui::Button(localize("form.create"))) { is_adding_item = false; is_editing_item = false; administration_cost_center_add(new_cost_center); ui_destroy_settings(); ui_setup_settings(); } if (!can_save) ImGui::EndDisabled(); ImGui::SameLine(); if (ImGui::Button(localize("form.cancel"))) { is_adding_item = false; is_editing_item = false; memset(&new_cost_center, 0, sizeof(new_cost_center)); } } ImGui::EndTable(); } // If not adding a new item already, show create button at bottom of list. if (!is_adding_item && ImGui::Button(localize("form.create"))) { new_cost_center = administration_cost_center_create_empty(); is_adding_item = true; is_editing_item = false; } } static void ui_draw_services() { // AI service if (ImGui::CollapsingHeader(localize("settings.services.ai_service"))) { // TODO: get this from iterator over ai_get_impl char* services[2] = { "OpenAI", "DeepSeek", }; if (ImGui::BeginCombo(localize("settings.services.ai_service.provider"), services[new_service.provider])) { for (u32 n = 0; n < 2; n++) { bool is_selected = n == (uint32_t)new_service.provider; if (ImGui::Selectable(services[n], is_selected)) { new_service.provider = (ai_provider)n; } } ImGui::EndCombo(); } ImGui::InputTextWithHint(localize("settings.services.ai_service.pubkey"), localize("settings.services.ai_service.pubkey"), new_service.api_key_public, sizeof(new_service.api_key_public)); if (ImGui::Button(localize("form.save"))) { administration_set_ai_service(new_service); } } } void ui_draw_settings() { if (ImGui::BeginTabBar("SettingsTabBar")) { if (ImGui::BeginTabItem(localize("settings.table.company"), nullptr, select_company_tab == 1 ? ImGuiTabItemFlags_SetSelected : 0)) { select_company_tab = 0; draw_contact_form(&company_info); // Save button. bool can_save = administration_contact_is_valid(company_info) == A_ERR_SUCCESS; if (!can_save) ImGui::BeginDisabled(); ImGui::Spacing(); if (ImGui::Button(localize("form.save"))) { administration_company_info_set(company_info); } if (!can_save) ImGui::EndDisabled(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(localize("settings.table.vatrates"))) { ui_draw_vat_rates(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(localize("settings.table.costcenters"))) { ui_draw_cost_centers(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem(localize("settings.table.services"))) { ui_draw_services(); ImGui::EndTabItem(); } ImGui::EndTabBar(); } }