/* * 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 "ui.hpp" #include "imgui.h" #include "memops.hpp" #include "strops.hpp" #include "locales.hpp" #include "importer.hpp" #include "administration.hpp" #include "administration_writer.hpp" static importer::invoice_request* active_import_request = 0; static ui::view_state current_view_state = ui::view_state::LIST_ALL; static invoice active_invoice = {0}; static invoice selected_for_removal = {0}; static billing_item* invoice_items_buffer = 0; void draw_addressee_form_ex(delivery_info* buffer, bool viewing_only = false); void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false); void draw_invoice_items_form(invoice* invoice); void ui::destroy_expenses() { memops::unalloc(invoice_items_buffer); } void ui::setup_expenses() { if (active_import_request != 0) { current_view_state = ui::view_state::VIEW_IMPORT_REQUEST; } else { current_view_state = ui::view_state::LIST_ALL; } active_invoice = administration::invoice_create_empty(); u32 invoice_items_count = MAX_BILLING_ITEMS; invoice_items_buffer = (billing_item*)memops::alloc(sizeof(billing_item) * invoice_items_count); } static void draw_expense_form(invoice* buffer, bool viewing_only = false) { if (viewing_only) ImGui::BeginDisabled(); ImGui::Text("%s: %s", locale::get("invoice.form.invoicenumber"), buffer->sequential_number); ImGui::Text("%s: %s", locale::get("invoice.form.billedTo"), buffer->customer.name); tm issued_at_date = *gmtime(&buffer->issued_at); if (ImGui::DatePicker("##issuedAt", issued_at_date)) { buffer->issued_at = mktime(&issued_at_date); } ImGui::SameLine(); ImGui::Text(locale::get("invoice.form.issuedat")); tm expires_at_date = *gmtime(&buffer->expires_at); if (ImGui::DatePicker("##expiresAt", expires_at_date)) { buffer->expires_at = mktime(&expires_at_date); } ImGui::SameLine(); ImGui::Text(locale::get("invoice.form.expiresat")); tm delivered_at_date = *gmtime(&buffer->delivered_at); if (ImGui::DatePicker("##deliveredAt", delivered_at_date)) { buffer->delivered_at = mktime(&delivered_at_date); } ImGui::SameLine(); ImGui::Text(locale::get("invoice.form.deliveredat")); ImGui::Separator(); if (ImGui::FormInvoiceFileSelector("Select file...", buffer->document.original_path)) { // @locale::get buffer->document.copy_path[0] = 0; } ImGui::Separator(); ImGui::Text(locale::get("invoice.form.supplier")); draw_contact_form_ex(&buffer->supplier, false, true); ImGui::Checkbox(locale::get("invoice.form.triangulation"), &buffer->is_triangulation); if (buffer->is_triangulation) { ImGui::Spacing(); ImGui::Text(locale::get("invoice.form.shippinginformation")); draw_addressee_form_ex(&buffer->addressee, 0); } ImGui::Separator(); ImGui::FormProjectCombo(buffer->project_id); ImGui::FormCostCenterCombo(buffer->cost_center_id); ImGui::Separator(); ImGui::Spacing(); ImGui::Spacing(); ImGui::Spacing(); bool max_items_reached = administration::billing_item_count(buffer) >= MAX_BILLING_ITEMS; if (max_items_reached) ImGui::BeginDisabled(); if (ImGui::Button(locale::get(locale::get("invoice.form.add")))) { billing_item item = administration::billing_item_create_empty(); administration::billing_item_add_to_invoice(buffer, item); } if (max_items_reached) ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Text("| %s: ", locale::get("invoice.form.currency")); ImGui::SameLine(); if (ImGui::FormCurrencyCombo(buffer->currency)) { administration::invoice_set_currency(buffer, buffer->currency); } draw_invoice_items_form(buffer); if (viewing_only) ImGui::EndDisabled(); } static void draw_expenses_list() { if (!administration::can_create_invoices()) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 102, 204, 255)); // blue ImGui::Text(locale::get("ui.invoiceRequirementP1")); ImGui::PopStyleColor(); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { ui::set_state(ui::main_state::UI_SETTINGS); } } ImGui::SameLine(); ImGui::Text(locale::get("ui.invoiceRequirementP2")); return; } const u32 items_per_page = 50; static s32 current_page = 0; invoice invoice_list[items_per_page]; u32 invoice_count = administration::invoice_get_partial_list_incomming(current_page, items_per_page, invoice_list); u32 total_invoice_count = administration::invoice_get_incomming_count(); s32 max_page = (total_invoice_count + items_per_page - 1) / items_per_page; if (max_page == 0) max_page = 1; // Table header controls: create, import, and pagination. if (ImGui::Button(locale::get("form.create"))) { current_view_state = ui::view_state::CREATE; 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; } char import_file_path[MAX_LEN_PATH] = {0}; ImGui::SameLine(); if (ImGui::FormInvoiceFileSelector("+ Import", import_file_path)) { // @locale::get current_view_state = ui::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; active_import_request = importer::ai_document_to_invoice(import_file_path); } if (current_page >= max_page-1) current_page = max_page-1; if (current_page < 0) current_page = 0; // Navigate to prev page button. ImGui::SameLine(); bool enable_prev = current_page > 0; if (!enable_prev) ImGui::BeginDisabled(); if (ImGui::Button(locale::get("ui.prev")) && current_page > 0) current_page--; if (!enable_prev) ImGui::EndDisabled(); ImGui::SameLine(); ImGui::Text("(%d/%d)", current_page+1, max_page); // Navigate to next page button. ImGui::SameLine(); bool enable_next = current_page < max_page-1; if (!enable_next) ImGui::BeginDisabled(); if (ImGui::Button(locale::get("ui.next")) && current_page < max_page-1) current_page++; if (!enable_next) ImGui::EndDisabled(); ImGui::Spacing(); if (ImGui::BeginTable("TableInvoices", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn(locale::get("invoice.table.invoicenumber"), ImGuiTableColumnFlags_WidthFixed, 120); ImGui::TableSetupColumn(locale::get("invoice.table.sender")); ImGui::TableSetupColumn(locale::get("invoice.table.customer")); ImGui::TableSetupColumn(locale::get("invoice.table.issuedat")); ImGui::TableSetupColumn(locale::get("invoice.table.total")); ImGui::TableSetupColumn(locale::get("invoice.table.status")); ImGui::TableSetupColumn(""); ImGui::TableHeadersRow(); for (u32 i = 0; i < invoice_count; i++) { invoice c = invoice_list[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::Text(c.sequential_number); ImGui::TableSetColumnIndex(1); ImGui::Text(c.supplier.name); ImGui::TableSetColumnIndex(2); ImGui::Text(c.customer.name); struct tm *lt = localtime(&c.issued_at); char buf[80]; strftime(buf, sizeof(buf), "%d-%m-%Y", lt); ImGui::TableSetColumnIndex(3); ImGui::Text(buf); ImGui::TableSetColumnIndex(4); ImGui::Text("%.2f %s", c.total, c.currency); ImGui::TableSetColumnIndex(5); ImGui::Text("%s", locale::get(administration::invoice_get_status_string(&c))); ImGui::TableSetColumnIndex(6); char btn_name[20]; strops::format(btn_name, sizeof(btn_name), "%s##%d", locale::get("form.view"), i); if (ImGui::Button(btn_name)) { active_invoice = c; current_view_state = ui::view_state::VIEW_EXISTING; } ImGui::SameLine(); strops::format(btn_name, sizeof(btn_name), "%s##%d", locale::get("form.change"), i); if (ImGui::Button(btn_name)) { active_invoice = administration::invoice_create_copy(&c); // We create a copy because of billing item list pointers. current_view_state = ui::view_state::EDIT_EXISTING; } ImGui::SameLine(); strops::format(btn_name, sizeof(btn_name), "%s##%d", locale::get("form.delete"), i); if (ImGui::Button(btn_name)) { selected_for_removal = c; ImGui::OpenPopup("ConfirmDeletePopup"); } } // Confirmation popup before contact is deleted definitively. if (ImGui::BeginPopupModal("ConfirmDeletePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoTitleBar)) { ImGui::Text(locale::get("form.confirmDelete")); ImGui::Separator(); if (ImGui::Button(locale::get("form.yes"), ImVec2(120, 0))) { administration::invoice_remove(&selected_for_removal); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button(locale::get("form.no"), ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } ImGui::EndTable(); } } static void draw_expense_update() { if (ImGui::Button(locale::get("form.back"))) { current_view_state = ui::view_state::LIST_ALL; } draw_expense_form(&active_invoice); bool can_save = administration::invoice_is_valid(&active_invoice) == A_ERR_SUCCESS; if (!can_save) ImGui::BeginDisabled(); ImGui::Spacing(); if (ImGui::Button(locale::get("form.save"))) { administration::invoice_update(&active_invoice); current_view_state = ui::view_state::LIST_ALL; ui::destroy_expenses(); ui::setup_expenses(); } if (!can_save) ImGui::EndDisabled(); } static void draw_expense_create() { if (ImGui::Button(locale::get("form.back"))) { current_view_state = ui::view_state::LIST_ALL; } draw_expense_form(&active_invoice); bool can_save = administration::invoice_is_valid(&active_invoice) == A_ERR_SUCCESS; if (!can_save) ImGui::BeginDisabled(); ImGui::Spacing(); if (ImGui::Button(locale::get("form.save"))) { administration::invoice_add(&active_invoice); current_view_state = ui::view_state::LIST_ALL; ui::destroy_expenses(); ui::setup_expenses(); } if (!can_save) ImGui::EndDisabled(); } static void draw_expense_view() { if (ImGui::Button(locale::get("form.back"))) { current_view_state = ui::view_state::LIST_ALL; } draw_expense_form(&active_invoice, true); } static void draw_import_request() { assert(active_import_request); if (active_import_request->status == importer::status::IMPORT_DONE) { if (active_import_request->error == I_ERR_SUCCESS) { active_invoice = active_import_request->result; current_view_state = ui::view_state::CREATE; active_import_request = 0; return; } else { if (ImGui::Button(locale::get("form.back"))) { current_view_state = ui::view_state::LIST_ALL; active_import_request = 0; return; } } } ImGui::PushFont(ui::fontBig); ImVec2 windowSize = ImGui::GetWindowSize(); float radius = 60.0f; const char* text = importer::status_to_string(active_import_request->status); if (active_import_request->error != I_ERR_SUCCESS) text = importer::error_to_string(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) { case ui::view_state::LIST_ALL: draw_expenses_list(); break; case ui::view_state::CREATE: draw_expense_create(); break; case ui::view_state::EDIT_EXISTING: draw_expense_update(); break; case ui::view_state::VIEW_EXISTING: draw_expense_view(); break; case ui::view_state::VIEW_IMPORT_REQUEST: draw_import_request(); break; } }