summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAldrik Ramaekers <aldrik@mailbox.org>2025-12-27 17:25:41 +0100
committerAldrik Ramaekers <aldrik@mailbox.org>2025-12-27 17:25:41 +0100
commit7c3a271feea4b3693bf93a47924f7c682585e179 (patch)
treeb72a1f3f2780f9f22f679e18f5d1780541cc040b
parent28c730a2e35ce81634dd4d47bf8e92e4552ec17c (diff)
mail provider settings ui
-rw-r--r--TODO3
-rw-r--r--include/administration.hpp16
-rw-r--r--include/file_templates.hpp4
-rw-r--r--include/importer.hpp9
-rwxr-xr-xrun_linux64.sh4
-rw-r--r--src/administration.cpp13
-rw-r--r--src/administration_reader.cpp5
-rw-r--r--src/administration_writer.cpp4
-rw-r--r--src/importer.cpp13
-rw-r--r--src/providers/DeepSeek.cpp (renamed from src/ai_providers/DeepSeek.cpp)0
-rw-r--r--src/providers/MailerSend.cpp35
-rw-r--r--src/providers/openAI.cpp (renamed from src/ai_providers/openAI.cpp)0
-rw-r--r--src/ui/imgui_extensions.cpp2
-rw-r--r--src/ui/ui_invoices.cpp4
-rw-r--r--src/ui/ui_settings.cpp74
15 files changed, 158 insertions, 28 deletions
diff --git a/TODO b/TODO
index 723dbf3..cdf8f16 100644
--- a/TODO
+++ b/TODO
@@ -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"))