From 4cfbd259d1a6fbe7592b8975eed399b46082edc1 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sat, 4 Oct 2025 09:25:36 +0200 Subject: import ui --- docs/CHANGES.rst | 4 +- include/ai_service.hpp | 36 ----------- include/import_service.hpp | 56 ++++++++++++++++ include/ui.hpp | 1 + libs/imgui-1.92.1/imgui.cpp | 40 ++++++++++++ libs/imgui-1.92.1/imgui.h | 4 ++ libs/xml.c/src/xml.c | 6 +- src/ai_providers/openAI.cpp | 20 +++--- src/ai_service.cpp | 95 --------------------------- src/import_service.cpp | 152 ++++++++++++++++++++++++++++++++++++++++++++ src/locales/en.cpp | 11 ++++ src/main.cpp | 2 +- src/ui/ui_expenses.cpp | 61 ++++++++++++++++-- 13 files changed, 338 insertions(+), 150 deletions(-) delete mode 100644 include/ai_service.hpp create mode 100644 include/import_service.hpp delete mode 100644 src/ai_service.cpp create mode 100644 src/import_service.cpp diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst index b93080c..2d1bbf1 100644 --- a/docs/CHANGES.rst +++ b/docs/CHANGES.rst @@ -2,13 +2,11 @@ TODO: for invoice importing using AI: + 1. my address data should be editable because import is not perfect 4. find billing item tax rate based on paid tax vs total 6. set document original and copy path (save file to zip first) 7. create a new invoice ID - 8. call ai_document_to_invoice in a thread & create a new UI page to show loading screen. - The user should not be able to add/update existing invoices but they can navigate to other screens - and come back to the loading screen. - retrieve available balance from AI api & show in settings/services. - let user choose the model to use in settings/services/ai - real error logging for OpenAI and importing in general diff --git a/include/ai_service.hpp b/include/ai_service.hpp deleted file mode 100644 index cf0b67a..0000000 --- a/include/ai_service.hpp +++ /dev/null @@ -1,36 +0,0 @@ -/* -* 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. -*/ - -#pragma once - -#include "administration.hpp" - -typedef struct -{ - time_t started_at; - bool finished; - char* result; -} ai_request; - -typedef struct -{ - bool (*upload_file)(char* file_path, char* file_id, size_t file_id_len); - bool (*query_with_file)(char* query, size_t query_length, char* file_id, char** response); -} ai_provider_impl; - -extern ai_provider_impl _chatgpt_api_provider; - -ai_request* ai_document_to_invoice(char* file_path); \ No newline at end of file diff --git a/include/import_service.hpp b/include/import_service.hpp new file mode 100644 index 0000000..516257e --- /dev/null +++ b/include/import_service.hpp @@ -0,0 +1,56 @@ +/* +* 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. +*/ + +#pragma once + +#include "administration.hpp" + +typedef uint32_t i_err; + +#define I_ERR_SUCCESS 0 +#define I_ERR_FAILED_UPLOAD 1 +#define I_ERR_FAILED_QUERY 2 +#define I_ERR_FAILED_IMPORT 3 + +typedef enum +{ + IMPORT_STARTING, + IMPORT_UPLOADING_FILE, + IMPORT_QUERYING, + IMPORT_WAITING_FOR_RESPONSE, + IMPORT_DONE, +} import_status; + +typedef struct +{ + time_t started_at; + invoice result; + char file_path[MAX_LEN_PATH]; + i_err error; + import_status status; +} import_invoice_request; + +typedef struct +{ + bool (*upload_file)(char* file_path, char* file_id, size_t file_id_len); + bool (*query_with_file)(char* query, size_t query_length, char* file_id, char** response); +} ai_provider_impl; + +extern ai_provider_impl _chatgpt_api_provider; + +const char* import_error_to_str(i_err error); +const char* import_status_to_str(import_status status); +import_invoice_request* ai_document_to_invoice(char* file_path); \ No newline at end of file diff --git a/include/ui.hpp b/include/ui.hpp index 33f6d18..931f302 100644 --- a/include/ui.hpp +++ b/include/ui.hpp @@ -47,6 +47,7 @@ typedef enum EDIT, CREATE, VIEW, + VIEW_IMPORT_REQUEST, } view_state; typedef struct diff --git a/libs/imgui-1.92.1/imgui.cpp b/libs/imgui-1.92.1/imgui.cpp index 99819ff..b41a167 100644 --- a/libs/imgui-1.92.1/imgui.cpp +++ b/libs/imgui-1.92.1/imgui.cpp @@ -16677,6 +16677,46 @@ void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_ EndTooltip(); } +// From https://github.com/ocornut/imgui/issues/1901#issuecomment-444929973 +void ImGui::LoadingIndicatorCircle(const char* label, const float indicator_radius, + const ImVec4& main_color, const ImVec4& backdrop_color, + const int circle_count, const float speed) { + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) { + return; + } + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + + const ImVec2 pos = window->DC.CursorPos; + const float circle_radius = indicator_radius / 15.0f; + const float updated_indicator_radius = indicator_radius - 4.0f * circle_radius; + const ImRect bb(pos, ImVec2(pos.x + indicator_radius * 2.0f, pos.y + indicator_radius * 2.0f)); + ItemSize(bb); + if (!ItemAdd(bb, id)) { + return; + } + const double t = g.Time; + const auto degree_offset = 2.0f * IM_PI / circle_count; + for (int i = 0; i < circle_count; ++i) { + const auto x = updated_indicator_radius * sin(degree_offset * i); + const auto y = updated_indicator_radius * cos(degree_offset * i); + + #define max(a,b) (((a)>(b))?(a):(b)) + + const auto growth = max(0.0f, sin(t * speed - i * degree_offset)); + ImVec4 color; + color.x = (float)(main_color.x * growth + backdrop_color.x * (1.0f - growth)); + color.y = (float)(main_color.y * growth + backdrop_color.y * (1.0f - growth)); + color.z = (float)(main_color.z * growth + backdrop_color.z * (1.0f - growth)); + color.w = 1.0f; + window->DrawList->AddCircleFilled(ImVec2((float)(pos.x + indicator_radius + x), + (float)(pos.y + indicator_radius - y)), + (float)(circle_radius + growth * circle_radius), GetColorU32(color)); + } +} + // Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. // In order to reduce interferences with the contents we are trying to debug into. bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) diff --git a/libs/imgui-1.92.1/imgui.h b/libs/imgui-1.92.1/imgui.h index a2b2a1f..29c89b9 100644 --- a/libs/imgui-1.92.1/imgui.h +++ b/libs/imgui-1.92.1/imgui.h @@ -375,6 +375,10 @@ IM_MSVC_RUNTIME_CHECKS_RESTORE namespace ImGui { + IMGUI_API void LoadingIndicatorCircle(const char* label, const float indicator_radius, + const ImVec4& main_color, const ImVec4& backdrop_color, + const int circle_count, const float speed); + // Context creation and access // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() diff --git a/libs/xml.c/src/xml.c b/libs/xml.c/src/xml.c index 3601aa6..ffebdb5 100644 --- a/libs/xml.c/src/xml.c +++ b/libs/xml.c/src/xml.c @@ -1269,7 +1269,8 @@ char* xml_get_str(struct xml_node* root, char* buffer, size_t bufsize, char* chi memset(buffer, 0, bufsize); struct xml_string* str = xml_node_content(node); - xml_string_copy(str, (uint8_t *)buffer, xml_string_length(str)); + xml_string_copy(str, (uint8_t *)buffer, bufsize); + buffer[bufsize-1] = 0; return buffer; } @@ -1284,7 +1285,8 @@ char* xml_get_str_x(struct xml_node* root, char* buffer, size_t bufsize, char* c memset(buffer, 0, bufsize); struct xml_string* str = xml_node_content(node); - xml_string_copy(str, (uint8_t *)buffer, xml_string_length(str)); + xml_string_copy(str, (uint8_t *)buffer, bufsize); + buffer[bufsize-1] = 0; return buffer; } diff --git a/src/ai_providers/openAI.cpp b/src/ai_providers/openAI.cpp index e005a80..7675e8b 100644 --- a/src/ai_providers/openAI.cpp +++ b/src/ai_providers/openAI.cpp @@ -24,15 +24,13 @@ #include "httplib.h" #include "strops.hpp" #include "log.hpp" -#include "ai_service.hpp" +#include "import_service.hpp" static bool _openAI_query_with_file(char* query, size_t query_length, char* file_id, char** response) { - //#define TESTING_IMPORT - - #ifndef TESTING_IMPORT + #if 0 const char *api_key = administration_get_ai_service().api_key_public; - + httplib::SSLClient cli("api.openai.com", 443); //cli.enable_server_certificate_verification(false); @@ -57,16 +55,17 @@ static bool _openAI_query_with_file(char* query, size_t query_length, char* file } char* response_body = (char*)res->body.c_str(); - #else - char* response_body = "{\n \"id\": \"resp_68d9482030fc8196930b43b6b28feeb104e98afee829eee0\",\n \"object\": \"response\",\n \"created_at\": 1759070240,\n \"status\": \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"error\": null,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-5-2025-08-07\",\n \"output\": [\n {\n \"id\": \"rs_68d94821d1f0819694533a6ed7ed6b2904e98afee829eee0\",\n \"type\": \"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_68d948a09e0c819696782e09c6b7626104e98afee829eee0\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"\\n urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0\\n urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\\n 586928\\n 2025-03-17\\n 2025-03-24\\n 380\\n EUR\\n \\n 699607\\n \\n \\n AR385893\\n Jouw bestelling : 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n R.C le mans B 302 494 224\\n \\n \\n Visserie-service\\n \\n \\n Z.A Nord\\n \\n Parce sur Sarthe\\n 72300\\n \\n \\n FR\\n \\n \\n \\n FR57 302 494 224\\n \\n VAT\\n \\n \\n \\n Visserie Service SAS\\n \\n \\n AMELIE L\\n 02.43.62.09.08\\n klantenservice@schroeven-express.nl\\n \\n \\n \\n \\n \\n \\n \\n cl585187\\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n VAT\\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n A RAMAEKERS\\n 31618260377\\n aldrikboy@gmail.com\\n \\n \\n \\n \\n 2025-03-17\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n \\n \\n \\n 586928\\n \\n FR76 1790 6001 1272 5017 0700 137\\n Visserie Service SAS\\n \\n \\n AGRIFRPP879\\n \\n \\n \\n \\n \\n \\n \\n \\n 2.59\\n \\n 12.36\\n 2.59\\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n \\n 6.95\\n 12.36\\n 14.95\\n 14.95\\n \\n \\n 1\\n 500\\n 6.95\\n \\n false\\n Discount\\n \\n \\n \\n \\n \\n Metalen schroeven RVS A2 gefreesde kop Pozi N\\u00b01 M2X4 DIN 965 ISO 7046, VS0109, VS0110\\n \\n Internal Tax Rate ID\\n \\n \\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n 1.39\\n \\n \\n\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n"; - #endif - *response = (char*)malloc(100000); memset(*response, 0, 100000); strncpy(*response, response_body, 100000); strops_get_json_value(*response, "text", *response, 100000); *response = strops_unprep_str_from_json(*response); + #else + *response = (char*)malloc(100000); + memset(*response, 0, 100000); + strops_copy(*response, " urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 492043632 2024-09-01 2024-09-01 380 USD Final invoice do:team:67840ecb-44e2-472e-bc45-801bd4e1f1fe DigitalOcean LLC 101 Avenue of the Americas 2nd Floor New York 10013 NY US EU528002224 VAT DigitalOcean LLC My Team Keerderstraat 81 Maastricht 6226 XW LI NL VAT aldrikboy@gmail.com 492043632 3.49 15.60 3.28 VAT 1.00 0.21 VAT 16.60 16.60 20.09 20.09 1 16.60 false Discount Product Usage Charges Internal Tax Rate ID VAT ", 100000); + #endif return 1; } @@ -98,6 +97,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); if (!res || res->status != 200) { log_error("ERROR Failed to create upload."); + log_error(res->body.c_str()); fclose(orig_file); return 0; } @@ -133,6 +133,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l if (!part_res || part_res->status != 200) { log_error("Failed to upload part %d.", part_number); + log_error(part_res->body.c_str()); free(buffer); fclose(orig_file); return 0; @@ -161,6 +162,7 @@ static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_l if (!complete_res || complete_res->status != 200) { log_error("ERROR Failed to complete upload."); + log_error(complete_res->body.c_str()); return 0; } diff --git a/src/ai_service.cpp b/src/ai_service.cpp deleted file mode 100644 index 82d81d6..0000000 --- a/src/ai_service.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/* -* 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. -*/ - -#define _CRT_SECURE_NO_WARNINGS - -#include -#include -#include - -#define CPPHTTPLIB_OPENSSL_SUPPORT -#include "httplib.h" -#include "log.hpp" -#include "ai_service.hpp" -#include "strops.hpp" -#include "administration_reader.hpp" - -ai_provider_impl _ai_get_impl() -{ - ai_provider provider = administration_get_ai_service().provider; - - switch(provider) - { - case AI_PROVIDER_OPENAI: return _chatgpt_api_provider; - default: assert(0); break; - } - - return ai_provider_impl {0}; -} - -extern const char* peppol_invoice_template; -extern const char* peppol_invoice_line_template; - -ai_request* ai_document_to_invoice(char* file_path) -{ - ai_provider_impl impl = _ai_get_impl(); - - char file_id[100]; - if (!impl.upload_file(file_path, file_id, 100)) { - return 0; - } - - size_t query_buffer_len = 50000; - char* template_buffer = (char*)malloc(query_buffer_len); - memset(template_buffer, 0, query_buffer_len); - - strncpy(template_buffer, peppol_invoice_template, query_buffer_len); - strops_replace(template_buffer, 50000, "{{INVOICE_LINE_LIST}}", peppol_invoice_line_template); - - char* ai_query = - "\n\nI have provided a file containing an invoice. Fill in the above Peppol 3.0 template with the information from the invoice." - "Do not add any fields to the template. If you can't find data for a given field, leave it empty. Do not make up any information." - "Only return the filled out template in valid XML format. Nothing else.\n"; - - size_t query_len = strlen(template_buffer); - strncpy(template_buffer + query_len, ai_query, query_buffer_len - query_len); - - char* response; - if (!impl.query_with_file(template_buffer, query_buffer_len, file_id, &response)) { - return 0; - } - - invoice inv; - if (!administration_reader_read_invoice_from_xml(&inv, response, strlen(response))) { - return false; - } - - invoice tmp = administration_invoice_create_empty(); - - inv.status = invoice_status::INVOICE_RECEIVED; - strops_copy(inv.id, tmp.id, MAX_LEN_ID); // TODO next_id is not being incremented - strops_copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID); // TODO param for incomming/exporting necessary - - strops_copy(inv.document.original_path, file_path, MAX_LEN_PATH); - strops_copy(inv.document.copy_path, "", MAX_LEN_PATH); - - a_err result = administration_invoice_import(&inv); - - free(template_buffer); - free(response); - - return 0; -} \ No newline at end of file diff --git a/src/import_service.cpp b/src/import_service.cpp new file mode 100644 index 0000000..b7a519c --- /dev/null +++ b/src/import_service.cpp @@ -0,0 +1,152 @@ +/* +* 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. +*/ + +#define _CRT_SECURE_NO_WARNINGS + +#include +#include +#include +#include + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" +#include "log.hpp" +#include "import_service.hpp" +#include "strops.hpp" +#include "administration_reader.hpp" +#include "locales.hpp" + +ai_provider_impl _ai_get_impl() +{ + ai_provider provider = administration_get_ai_service().provider; + + switch(provider) + { + case AI_PROVIDER_OPENAI: return _chatgpt_api_provider; + default: assert(0); break; + } + + return ai_provider_impl {0}; +} + +extern const char* peppol_invoice_template; +extern const char* peppol_invoice_line_template; + +static int _ai_document_to_invoice_t(void *arg) { + import_invoice_request* request = (import_invoice_request*)arg; + char* file_path = request->file_path; + ai_provider_impl impl = _ai_get_impl(); + + request->status = import_status::IMPORT_UPLOADING_FILE; + + char file_id[100]; + if (!impl.upload_file(file_path, file_id, 100)) { + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_UPLOAD; + return 0; + } + + request->status = import_status::IMPORT_QUERYING; + + size_t query_buffer_len = 50000; + char* template_buffer = (char*)malloc(query_buffer_len); + memset(template_buffer, 0, query_buffer_len); + + strncpy(template_buffer, peppol_invoice_template, query_buffer_len); + strops_replace(template_buffer, 50000, "{{INVOICE_LINE_LIST}}", peppol_invoice_line_template); + + char* ai_query = + "\n\nI have provided a file containing an invoice. Fill in the above Peppol 3.0 template with the information from the invoice." + "Do not add any fields to the template. If you can't find data for a given field, leave it empty. Do not make up any information." + "Only return the filled out template in valid XML format. Nothing else.\n"; + + size_t query_len = strlen(template_buffer); + strncpy(template_buffer + query_len, ai_query, query_buffer_len - query_len); + + request->status = import_status::IMPORT_WAITING_FOR_RESPONSE; + + char* response; + if (!impl.query_with_file(template_buffer, query_buffer_len, file_id, &response)) { + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_QUERY; + return 0; + } + + invoice inv; + if (!administration_reader_read_invoice_from_xml(&inv, response, strlen(response))) { + request->status = import_status::IMPORT_DONE; + request->error = I_ERR_FAILED_IMPORT; + return 0; + } + + invoice tmp = administration_invoice_create_empty(); + + inv.status = invoice_status::INVOICE_RECEIVED; + strops_copy(inv.id, tmp.id, MAX_LEN_ID); // TODO next_id is not being incremented + contact my_info = administration_company_info_get(); + memcpy(&inv.customer, &my_info, sizeof(contact)); + strops_copy(inv.customer.id, MY_COMPANY_ID, MAX_LEN_ID); + + strops_copy(inv.document.original_path, file_path, MAX_LEN_PATH); + strops_copy(inv.document.copy_path, "", MAX_LEN_PATH); + + free(template_buffer); + free(response); + + request->status = import_status::IMPORT_DONE; + request->result = administration_invoice_create_copy(&inv); + return 0; +} + +import_invoice_request* ai_document_to_invoice(char* file_path) +{ + import_invoice_request* result = (import_invoice_request*)malloc(sizeof(import_invoice_request)); + result->started_at = time(NULL); + result->error = I_ERR_SUCCESS; + result->status = import_status::IMPORT_STARTING; + strops_copy(result->file_path, file_path, MAX_LEN_PATH); + + thrd_t thr; + if (thrd_create(&thr, _ai_document_to_invoice_t, result) != thrd_success) { + return 0; + } + + return result; +} + +const char* import_status_to_str(import_status status) +{ + switch(status) + { + case import_status::IMPORT_STARTING: return localize("import.status.starting"); + case import_status::IMPORT_UPLOADING_FILE: return localize("import.status.uploading_file"); + case import_status::IMPORT_QUERYING: return localize("import.status.querying"); + case import_status::IMPORT_WAITING_FOR_RESPONSE: return localize("import.status.waiting_for_result"); + case import_status::IMPORT_DONE: return localize("import.status.done"); + } + return ""; +} + +const char* import_error_to_str(i_err error) +{ + switch(error) + { + case I_ERR_FAILED_UPLOAD: return localize("import.error.upload"); + case I_ERR_FAILED_QUERY: return localize("import.error.query"); + case I_ERR_FAILED_IMPORT: return localize("import.error.import"); + } + return ""; +} \ No newline at end of file diff --git a/src/locales/en.cpp b/src/locales/en.cpp index 0152928..d311b36 100644 --- a/src/locales/en.cpp +++ b/src/locales/en.cpp @@ -196,6 +196,17 @@ locale_entry en_locales[] = { {"statement.tax", "Tax"}, {"statement.expenses", "Expenses"}, {"statement.profit", "Profit"}, + + // Import service. + {"import.status.starting","Starting import"}, + {"import.status.uploading_file","Uploading file"}, + {"import.status.querying","Querying AI provider"}, + {"import.status.waiting_for_result","Waiting for result"}, + {"import.status.done","Import completed"}, + + {"import.error.upload","Failure: Upload failed"}, + {"import.error.query","Failure: Querying service failed"}, + {"import.error.import","Failure: Failed to import result from service"}, }; const int en_locale_count = sizeof(en_locales) / sizeof(en_locales[0]); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index bdddd98..8475008 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -129,7 +129,7 @@ int main(int argc, char** argv) io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf"); //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\seguisym.ttf"); fontBold = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf"); - fontBig = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf", 36); + fontBig = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeuib.ttf", 30); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf"); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf"); //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf"); diff --git a/src/ui/ui_expenses.cpp b/src/ui/ui_expenses.cpp index 153e6b7..c02cdce 100644 --- a/src/ui/ui_expenses.cpp +++ b/src/ui/ui_expenses.cpp @@ -27,7 +27,9 @@ #include "administration.hpp" #include "administration_writer.hpp" #include "locales.hpp" -#include "ai_service.hpp" +#include "import_service.hpp" + +static import_invoice_request* active_import_request = 0; static view_state current_view_state = view_state::LIST; static invoice active_invoice = {0}; @@ -46,7 +48,13 @@ void ui_destroy_expenses() void ui_setup_expenses() { - current_view_state = view_state::LIST; + if (active_import_request != 0) { + current_view_state = view_state::VIEW_IMPORT_REQUEST; + } + else { + current_view_state = view_state::LIST; + } + active_invoice = administration_invoice_create_empty(); u32 invoice_items_count = MAX_BILLING_ITEMS; @@ -176,13 +184,13 @@ static void ui_draw_expenses_list() char import_file_path[MAX_LEN_PATH] = {0}; ImGui::SameLine(); if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @localize - //current_view_state = view_state::CREATE; + current_view_state = view_state::VIEW_IMPORT_REQUEST; active_invoice = administration_invoice_create_empty(); // @leak active_invoice.customer = administration_company_info_get(); active_invoice.is_outgoing = 0; active_invoice.status = invoice_status::INVOICE_RECEIVED; - ai_document_to_invoice(import_file_path); + active_import_request = ai_document_to_invoice(import_file_path); } if (current_page >= max_page-1) current_page = max_page-1; @@ -336,6 +344,50 @@ static void ui_draw_expense_view() draw_expense_form(&active_invoice, true); } +static void ui_draw_import_request() +{ + assert(active_import_request); + + if (active_import_request->status == import_status::IMPORT_DONE) { + if (active_import_request->error == I_ERR_SUCCESS) { + active_invoice = active_import_request->result; + current_view_state = view_state::CREATE; + active_import_request = 0; + return; + } + else { + if (ImGui::Button(localize("form.back"))) { + current_view_state = view_state::LIST; + active_import_request = 0; + return; + } + } + } + + ImGui::PushFont(fontBig); + + ImVec2 windowSize = ImGui::GetWindowSize(); + float radius = 60.0f; + + const char* text = import_status_to_str(active_import_request->status); + if (active_import_request->error != I_ERR_SUCCESS) text = import_error_to_str(active_import_request->error); + ImVec2 textSize = ImGui::CalcTextSize(text); + ImGui::SetCursorPos(ImVec2((windowSize.x - textSize.x) * 0.5f, + (windowSize.y) * 0.5f - radius - 40.0f)); + ImGui::Text(text); + + if (active_import_request->error == I_ERR_SUCCESS) { + ImGui::SetCursorPos(ImVec2((windowSize.x - radius*2) * 0.5f, + (windowSize.y - radius*2) * 0.5f)); + + const ImVec4 col = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered); + const ImVec4 bg = ImGui::GetStyleColorVec4(ImGuiCol_Button); + ImGui::LoadingIndicatorCircle("##loadingAnim", radius, bg, col, 10, 4.0f); + } + + ImGui::PopFont(); +} + void ui_draw_expenses() { switch(current_view_state) @@ -344,5 +396,6 @@ void ui_draw_expenses() case view_state::CREATE: ui_draw_expense_create(); break; case view_state::EDIT: ui_draw_expense_update(); break; case view_state::VIEW: ui_draw_expense_view(); break; + case view_state::VIEW_IMPORT_REQUEST: ui_draw_import_request(); break; } } \ No newline at end of file -- cgit v1.2.3-70-g09d2