diff options
| author | Aldrik Ramaekers <aldrik@mailbox.org> | 2025-12-27 17:25:41 +0100 |
|---|---|---|
| committer | Aldrik Ramaekers <aldrik@mailbox.org> | 2025-12-27 17:25:41 +0100 |
| commit | 7c3a271feea4b3693bf93a47924f7c682585e179 (patch) | |
| tree | b72a1f3f2780f9f22f679e18f5d1780541cc040b | |
| parent | 28c730a2e35ce81634dd4d47bf8e92e4552ec17c (diff) | |
mail provider settings ui
| -rw-r--r-- | TODO | 3 | ||||
| -rw-r--r-- | include/administration.hpp | 16 | ||||
| -rw-r--r-- | include/file_templates.hpp | 4 | ||||
| -rw-r--r-- | include/importer.hpp | 9 | ||||
| -rwxr-xr-x | run_linux64.sh | 4 | ||||
| -rw-r--r-- | src/administration.cpp | 13 | ||||
| -rw-r--r-- | src/administration_reader.cpp | 5 | ||||
| -rw-r--r-- | src/administration_writer.cpp | 4 | ||||
| -rw-r--r-- | src/importer.cpp | 13 | ||||
| -rw-r--r-- | src/providers/DeepSeek.cpp (renamed from src/ai_providers/DeepSeek.cpp) | 0 | ||||
| -rw-r--r-- | src/providers/MailerSend.cpp | 35 | ||||
| -rw-r--r-- | src/providers/openAI.cpp (renamed from src/ai_providers/openAI.cpp) | 0 | ||||
| -rw-r--r-- | src/ui/imgui_extensions.cpp | 2 | ||||
| -rw-r--r-- | src/ui/ui_invoices.cpp | 4 | ||||
| -rw-r--r-- | src/ui/ui_settings.cpp | 74 |
15 files changed, 158 insertions, 28 deletions
@@ -10,9 +10,6 @@ Testing: - write tests for strops.hpp - write tests that check error handling for corrupt files. (e.g. references to tax rates, project and cost center that failed to load) -Improvements: -- "price includes tax" toggle that recalculates billing item tax, net and total - Features: - Timeline for invoice modifications (e.g. edited, status changed, paid) - When creating a new openbooks file, user should only be able to set company info before anything else. diff --git a/include/administration.hpp b/include/administration.hpp index 31044f5..4f276ad 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -400,6 +400,19 @@ typedef struct char api_key_public[MAX_LEN_API_KEY]; } ai_service; +typedef enum +{ + EMAIL_PROVIDER_MAILERSEND = 0, + + EMAIL_PROVIDER_END, +} email_provider; + +typedef struct +{ + email_provider provider; + char api_key[MAX_LEN_API_KEY]; +} email_service; + typedef struct { time_t timestamp; @@ -433,6 +446,7 @@ typedef struct // Service providers. ai_service ai_service; + email_service email_service; } ledger; // Add/Update result codes. @@ -501,10 +515,12 @@ namespace administration { char* get_default_currency(); time_t get_default_invoice_expire_duration(); ai_service get_ai_service(); + email_service get_email_service(); void set_file_path(char* path); void set_next_id(s32 nr); void set_next_sequence_number(s32 nr); void set_ai_service(ai_service provider); + void set_email_service(email_service provider); void create_income_statement(income_statement* statement); bool can_create_invoices(); diff --git a/include/file_templates.hpp b/include/file_templates.hpp index 15637a7..c5e0191 100644 --- a/include/file_templates.hpp +++ b/include/file_templates.hpp @@ -71,6 +71,10 @@ namespace file_template { " <PublicKey>{{AI_SERVICE_PUBLIC_KEY}}</PublicKey>\n" " <Model>{{AI_SERVICE_MODEL}}</Model>\n" " </AIService>\n" + " <EmailService>\n" + " <Provider>{{EMAIL_SERVICE_PROVIDER}}</Provider>\n" + " <PublicKey>{{EMAIL_SERVICE_KEY}}</PublicKey>\n" + " </EmailService>\n" "</Administration>"; static const char* peppol_invoice_tax_subtotal_template = diff --git a/include/importer.hpp b/include/importer.hpp index 1d97c12..8db70ef 100644 --- a/include/importer.hpp +++ b/include/importer.hpp @@ -30,7 +30,6 @@ typedef uint32_t i_err; namespace importer { - typedef enum { IMPORT_STARTING, @@ -71,10 +70,18 @@ namespace importer { bool (*get_available_models)(model_list_request* buffer); } ai_provider_impl; + typedef struct + { + char* provider_name; + bool (*send_email)(char* sender, char* recipients, u32 recipients_count, const char* subject, const char* text); + } email_provider_impl; + const char* error_to_string(i_err error); const char* status_to_string(status status); ai_provider_impl get_ai_provider_implementation(ai_provider provider); + email_provider_impl get_email_provider_implementation(email_provider provider); + invoice_request* ai_document_to_invoice(char* file_path); model_list_request* ai_get_available_models(ai_provider service); }
\ No newline at end of file diff --git a/run_linux64.sh b/run_linux64.sh index b84875b..0cc70b3 100755 --- a/run_linux64.sh +++ b/run_linux64.sh @@ -12,7 +12,7 @@ libs/zip/src/*.c \ libs/xml.c/src/*.c \ libs/timer_lib/*.c \ libs/tinyfiledialogs/tinyfiledialogs.c" -SOURCES="src/*.cpp src/ui/*.cpp src/locales/*.cpp src/ai_providers/*.cpp" +SOURCES="src/*.cpp src/ui/*.cpp src/locales/*.cpp src/providers/*.cpp" LIBS="-lstdc++ -lglfw -lGL -lm -lssl -lcrypto" FLAGS="-Wall -Wno-changes-meaning -Wno-write-strings -Wno-attributes -Wno-unused-variable -fpermissive -Wno-format-zero-length -g" INCLUDE_DIRS="-Ilibs/imgui-1.92.1 \ @@ -31,7 +31,7 @@ DEFINITIONS="-D_PLATFORM_=\"linux64\"" # Check for test flag if [ "$1" == "-t" ]; then - SOURCES="tests/main.cpp src/administration.cpp src/administration_writer.cpp src/administration_reader.cpp src/strops.cpp src/logger.cpp src/locales.cpp src/locales/*.cpp src/ai_providers/*.cpp src/importer.cpp src/memops.cpp src/countries.cpp" + SOURCES="tests/main.cpp src/administration.cpp src/administration_writer.cpp src/administration_reader.cpp src/strops.cpp src/logger.cpp src/locales.cpp src/locales/*.cpp src/providers/*.cpp src/importer.cpp src/memops.cpp src/countries.cpp" OUT_EXE="accounting_tests" DEFINITIONS="-D_PLATFORM_=\"linux64\" -D_TESTING_MODE_" fi diff --git a/src/administration.cpp b/src/administration.cpp index b4f626e..61e5b3e 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -237,6 +237,17 @@ ai_service administration::get_ai_service() return g_administration.ai_service; } +email_service administration::get_email_service() +{ + return g_administration.email_service; +} + +void administration::set_email_service(email_service provider) +{ + g_administration.email_service = provider; + if (administration_data_changed_event_callback) administration_data_changed_event_callback(); +} + void administration::set_ai_service(ai_service provider) { g_administration.ai_service = provider; @@ -1899,8 +1910,6 @@ a_err administration::billing_item_add_to_invoice(invoice* invoice, billing_item memops::copy(tb, &item, sizeof(billing_item)); strops::format(tb->id, sizeof(tb->id), "B/%d", create_id()); strops::copy(tb->currency, invoice->currency, MAX_LEN_CURRENCY); // Set billing item currency to invoice currency. - - logger::info("XD: %s\n", tb->tax_internal_code); administration_recalculate_billing_item_totals(tb); if (!list_append(&invoice->billing_items, tb)) { diff --git a/src/administration_reader.cpp b/src/administration_reader.cpp index 34cfe8f..7528711 100644 --- a/src/administration_reader.cpp +++ b/src/administration_reader.cpp @@ -426,6 +426,11 @@ bool administration_reader::import_administration_info(char* buffer, size_t buff xml_get_str_x(root, ai_service.model_name, MAX_LEN_SHORT_DESC, "AIService", "Model", 0); administration::set_ai_service(ai_service); + email_service email_service; + email_service.provider = (email_provider)xml_get_s32_x(root, "EmailService", "Provider", 0); + xml_get_str_x(root, email_service.api_key, MAX_LEN_API_KEY, "EmailService", "PublicKey", 0); + administration::set_email_service(email_service); + logger::info("Loaded administration info in %.3fms. next_id=%d next_sequence_number=%d", STOPWATCH_TIME, administration::get_next_id(), administration::get_next_sequence_number()); diff --git a/src/administration_writer.cpp b/src/administration_writer.cpp index 45c8cdd..ef604b1 100644 --- a/src/administration_writer.cpp +++ b/src/administration_writer.cpp @@ -887,6 +887,10 @@ bool administration_writer::save_administration_info_blocking() strops::replace(file_content, buf_length, "{{AI_SERVICE_PUBLIC_KEY}}", ai_service.api_key_public); strops::replace(file_content, buf_length, "{{AI_SERVICE_MODEL}}", ai_service.model_name); + email_service email_service = administration::get_email_service(); + strops::replace_int32(file_content, buf_length, "{{EMAIL_SERVICE_PROVIDER}}", (s32)email_service.provider); + strops::replace(file_content, buf_length, "{{EMAIL_SERVICE_KEY}}", email_service.api_key); + //// Write to Disk. int final_length = (int)strops::length(file_content); if (!xml_string_is_valid((uint8_t*)file_content, final_length)) result = 0; diff --git a/src/importer.cpp b/src/importer.cpp index 3c56062..45fb16c 100644 --- a/src/importer.cpp +++ b/src/importer.cpp @@ -29,6 +29,8 @@ extern importer::ai_provider_impl _chatgpt_api_provider; extern importer::ai_provider_impl _deepseek_api_provider; +extern importer::email_provider_impl _mailersend_api_provider; + importer::ai_provider_impl importer::get_ai_provider_implementation(ai_provider provider) { switch(provider) @@ -41,6 +43,17 @@ importer::ai_provider_impl importer::get_ai_provider_implementation(ai_provider return importer::ai_provider_impl {0}; } +importer::email_provider_impl importer::get_email_provider_implementation(email_provider provider) +{ + switch(provider) + { + case EMAIL_PROVIDER_MAILERSEND: return _mailersend_api_provider; + default: assert(0); break; + } + + return importer::email_provider_impl {0}; +} + static void _batch_query_response_handler(invoice* buffer, char* json) { int alloc_size = 1000; diff --git a/src/ai_providers/DeepSeek.cpp b/src/providers/DeepSeek.cpp index c34e299..c34e299 100644 --- a/src/ai_providers/DeepSeek.cpp +++ b/src/providers/DeepSeek.cpp diff --git a/src/providers/MailerSend.cpp b/src/providers/MailerSend.cpp new file mode 100644 index 0000000..961e457 --- /dev/null +++ b/src/providers/MailerSend.cpp @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2025 Aldrik Ramaekers <aldrik.ramaekers@gmail.com> +* +* 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 <threads.h> + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" + +#include "memops.hpp" +#include "strops.hpp" +#include "logger.hpp" +#include "importer.hpp" + +bool _MailerSend_send_email(char* sender, char* recipients, u32 recipients_count, const char* subject, const char* text) +{ + return false; +} + +importer::email_provider_impl _mailersend_api_provider = { + "MailerSend", + _MailerSend_send_email, +};
\ No newline at end of file diff --git a/src/ai_providers/openAI.cpp b/src/providers/openAI.cpp index d1495dc..d1495dc 100644 --- a/src/ai_providers/openAI.cpp +++ b/src/providers/openAI.cpp diff --git a/src/ui/imgui_extensions.cpp b/src/ui/imgui_extensions.cpp index a90549f..f8adb70 100644 --- a/src/ui/imgui_extensions.cpp +++ b/src/ui/imgui_extensions.cpp @@ -525,7 +525,7 @@ namespace ImGui } int selected_tax_rate_index = -1; - char rate_str_buf[MAX_LEN_LONG_DESC]; + char rate_str_buf[MAX_LEN_LONG_DESC] = {0}; if (selected_tax_rate) { char category_code_desc[MAX_LEN_LONG_DESC]; diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp index c4d393c..dc51fd6 100644 --- a/src/ui/ui_invoices.cpp +++ b/src/ui/ui_invoices.cpp @@ -503,9 +503,9 @@ static void draw_invoice_view() if (ImGui::Selectable(locale::get("ui.sendAs.email"), false)) { } - if (ImGui::Selectable(locale::get("ui.sendAs.einvoice"), false)) { + // if (ImGui::Selectable(locale::get("ui.sendAs.einvoice"), false)) { - } + // } ImGui::EndCombo(); } ImGui::PushItemWidth(0.0f); diff --git a/src/ui/ui_settings.cpp b/src/ui/ui_settings.cpp index c458260..240e376 100644 --- a/src/ui/ui_settings.cpp +++ b/src/ui/ui_settings.cpp @@ -33,7 +33,8 @@ u32 cost_center_count; cost_center* cost_centers = 0; static int select_company_tab = 0; -static ai_service new_service; +static ai_service new_ai_service; +static email_service new_email_service; void ui::destroy_settings() { @@ -54,7 +55,8 @@ void ui::setup_settings() cost_centers = (cost_center*)memops::alloc(cost_center_count * sizeof(cost_center)); administration::cost_center_get_all(cost_centers); - new_service = administration::get_ai_service(); + new_ai_service = administration::get_ai_service(); + new_email_service = administration::get_email_service(); } } @@ -242,12 +244,13 @@ static void draw_cost_centers() } } -static void draw_services() -{ +// Dropdown to select ai service. +// If a new service is selected -> model is set to first result in available model list. +static void draw_ai_service_ui() +{ static importer::model_list_request* model_request = 0; static bool set_model_on_load = false; - // AI service if (ImGui::CollapsingHeader(locale::get("settings.services.ai_service"))) { char* ai_service_names[AI_PROVIDER_END]; @@ -255,13 +258,13 @@ static void draw_services() ai_service_names[i] = importer::get_ai_provider_implementation((ai_provider)i).provider_name; } - if (ImGui::BeginCombo(locale::get("settings.services.ai_service.provider"), ai_service_names[new_service.provider])) + if (ImGui::BeginCombo(locale::get("settings.services.ai_service.provider"), ai_service_names[new_ai_service.provider])) { for (u32 n = 0; n < AI_PROVIDER_END; n++) { - bool is_selected = n == (uint32_t)new_service.provider; + bool is_selected = n == (uint32_t)new_ai_service.provider; if (ImGui::Selectable(ai_service_names[n], is_selected)) { - new_service.provider = (ai_provider)n; + new_ai_service.provider = (ai_provider)n; model_request = 0; set_model_on_load = true; } @@ -270,10 +273,10 @@ static void draw_services() } ImGui::InputTextWithHint(locale::get("settings.services.ai_service.pubkey"), locale::get("settings.services.ai_service.pubkey"), - new_service.api_key_public, sizeof(new_service.api_key_public)); + new_ai_service.api_key_public, sizeof(new_ai_service.api_key_public)); if (!model_request) { - model_request = importer::ai_get_available_models(new_service.provider); + model_request = importer::ai_get_available_models(new_ai_service.provider); } // Default to first result in model list, or hardcoded default model. @@ -282,22 +285,22 @@ static void draw_services() set_model_on_load = false; if (model_request->result_count > 0) { - strops::copy(new_service.model_name, model_request->result[0], sizeof(new_service.model_name)); + strops::copy(new_ai_service.model_name, model_request->result[0], sizeof(new_ai_service.model_name)); } else { - strops::copy(new_service.model_name, importer::get_ai_provider_implementation(new_service.provider).default_model, sizeof(new_service.model_name)); + strops::copy(new_ai_service.model_name, importer::get_ai_provider_implementation(new_ai_service.provider).default_model, sizeof(new_ai_service.model_name)); } } } if (model_request->status == importer::status::IMPORT_DONE && model_request->error == I_ERR_SUCCESS) { - if (ImGui::BeginCombo(locale::get("settings.services.ai_service.model"), new_service.model_name)) + if (ImGui::BeginCombo(locale::get("settings.services.ai_service.model"), new_ai_service.model_name)) { for (u32 n = 0; n < model_request->result_count; n++) { - bool is_selected = strops::equals(new_service.model_name, model_request->result[n]); + bool is_selected = strops::equals(new_ai_service.model_name, model_request->result[n]); if (ImGui::Selectable(model_request->result[n], is_selected)) { - strops::copy(new_service.model_name, model_request->result[n], sizeof(new_service.model_name)); + strops::copy(new_ai_service.model_name, model_request->result[n], sizeof(new_ai_service.model_name)); } } ImGui::EndCombo(); @@ -317,7 +320,7 @@ static void draw_services() ImGui::SameLine(); } - ImGui::TextUnformatted(new_service.model_name); + ImGui::TextUnformatted(new_ai_service.model_name); ImGui::EndComboPreview(); } @@ -329,11 +332,48 @@ static void draw_services() if (ImGui::Button(locale::get("form.save"), true)) { administration_writer::set_write_completed_event_callback(0); - administration::set_ai_service(new_service); + administration::set_ai_service(new_ai_service); } } } +static void draw_email_service_ui() +{ + if (ImGui::CollapsingHeader(locale::get("settings.services.email_service"))) + { + char* email_service_names[EMAIL_PROVIDER_END]; + for (u32 i = 0; i < EMAIL_PROVIDER_END; i++) { + email_service_names[i] = importer::get_email_provider_implementation((email_provider)i).provider_name; + } + + if (ImGui::BeginCombo(locale::get("settings.services.email_service.provider"), email_service_names[new_ai_service.provider])) + { + for (u32 n = 0; n < EMAIL_PROVIDER_END; n++) + { + bool is_selected = n == (uint32_t)new_email_service.provider; + if (ImGui::Selectable(email_service_names[n], is_selected)) { + new_email_service.provider = (email_provider)n; + } + } + ImGui::EndCombo(); + } + + ImGui::InputTextWithHint(locale::get("settings.services.email_service.pubkey"), + locale::get("settings.services.email_service.pubkey"), new_email_service.api_key, sizeof(new_email_service.api_key)); + + if (ImGui::Button(locale::get("form.save"), true)) { + administration_writer::set_write_completed_event_callback(0); + administration::set_email_service(new_email_service); + } + } +} + +static void draw_services() +{ + draw_email_service_ui(); + draw_ai_service_ui(); +} + void ui::draw_settings() { if (ImGui::BeginTabBar("SettingsTabBar")) |
