/* * 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 activity* activity_buffer = 0; static u32 activity_count; 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 const float sidepanel_width = 200.0f; static billing_item* invoice_items_buffer = 0; void draw_invoice_items_form(invoice* invoice, bool outgoing = true); static void _reload_activities() { activity_count = administration::activity_get_all_for_object(activity_buffer, active_invoice.id); } static void _set_active_invoice(invoice inv) { active_invoice = inv; _reload_activities(); } void ui::destroy_expenses() { memops::unalloc(invoice_items_buffer); memops::unalloc(activity_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; } activity_buffer = (activity*)memops::alloc(sizeof(activity) * 250); _set_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::FileSelect(locale::get("ui.fileselect.text"), buffer->document.original_path)) { buffer->document.copy_path[0] = 0; } ImGui::Separator(); ImGui::Text(locale::get("invoice.form.billinginformation")); ImGui::ContactForm(&buffer->customer, false, true, false); ImGui::Separator(); ImGui::Text(locale::get("invoice.form.supplier")); ImGui::ContactForm(&buffer->supplier, false, true, false); ImGui::Checkbox(locale::get("invoice.form.triangulation"), &buffer->is_triangulation); if (buffer->is_triangulation) { ImGui::Spacing(); ImGui::Text(locale::get("invoice.form.shippinginformation")); ImGui::DeliveryInfoForm(&buffer->addressee, 0); } ImGui::Separator(); ImGui::ProjectDropdown(buffer->project_id); ImGui::CostCenterDropdown(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::CurrencyDropdown(buffer->currency)) { administration::invoice_set_currency(buffer, buffer->currency); } draw_invoice_items_form(buffer, false); 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(); 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::FileSelect(locale::get("ui.import"), import_file_path)) { current_view_state = ui::view_state::VIEW_IMPORT_REQUEST; active_invoice = administration::invoice_create_empty(); 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", 6, 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"), ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn(locale::get("invoice.table.total"), ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableSetupColumn(locale::get("invoice.table.status"), ImGuiTableColumnFlags_WidthFixed, 90); ImGui::TableHeadersRow(); for (u32 i = 0; i < invoice_count; i++) { invoice c = invoice_list[i]; ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); ImGui::PushID(i); ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap; bool selected = false; if (ImGui::Selectable("##invisible_selectable", selected, selectable_flags, ImVec2(0, ImGui::GetFrameHeight()-4.0f))) { _set_active_invoice(c); current_view_state = ui::view_state::VIEW_EXISTING; } ImGui::SetItemAllowOverlap(); ImGui::SameLine(); ImGui::PopID(); ImGui::Text(c.sequential_number); if (administration::invoice_is_valid(&c) != A_ERR_SUCCESS) { if (ImGui::WarningIcon(8.0f)) { ImGui::SetTooltip(locale::get("ui.tooltip.invalidInvoice")); } } 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))); /* 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"); }*/ } ImGui::EndTable(); } } static void _reset_to_default_view() { current_view_state = ui::view_state::LIST_ALL; ui::destroy_expenses(); ui::setup_expenses(); activity_count = 0; } static void _reset_to_invoice_view() { current_view_state = ui::view_state::VIEW_EXISTING; } static void _draw_activity_sidepanel() { ImGui::SameLine(); ImGui::SeparatorEx(1 << 1, 3.0f); ImGui::SameLine(); ImGui::BeginChild("##historyPanel", ImVec2(sidepanel_width, 0), ImGuiChildFlags_None); { for (u32 i = 0; i < activity_count; i++) { activity ac = activity_buffer[i]; char time_buffer[26]; struct tm* tm_info; tm_info = localtime(&ac.timestamp); strftime(time_buffer, 26, "%d/%m/%Y %H:%M", tm_info); ImGui::Text(time_buffer); ImGui::PushFont(ui::fontBold); ImGui::Text(ac.user_name); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextWrapped("%s", locale::get(ac.message)); ImGui::Spacing(); } } ImGui::EndChild(); } static void draw_expense_update() { if (ImGui::Button(locale::get("form.cancel"), true, false)) { current_view_state = ui::view_state::VIEW_EXISTING; } { // Save button bool can_save = administration::invoice_is_valid(&active_invoice) == A_ERR_SUCCESS; if (!can_save) ImGui::BeginDisabled(); ImGui::SameLine(); if (ImGui::Button(locale::get("form.save"), true)) { administration_writer::set_write_completed_event_callback(_reset_to_invoice_view); administration::invoice_update(&active_invoice); _reload_activities(); } if (!can_save) ImGui::EndDisabled(); } ImGui::Spacing(); ImGui::Separator(3.0f); ImGui::Spacing(); float availableWidth = ImGui::GetContentRegionAvail().x; ImGui::BeginChild("##invoicePanel", ImVec2(availableWidth - sidepanel_width, 0), ImGuiChildFlags_None); draw_expense_form(&active_invoice); ImGui::EndChild(); _draw_activity_sidepanel(); } static void draw_expense_create() { if (ImGui::Button(locale::get("form.back"), true, false)) { _reset_to_default_view(); } { // Save button bool can_save = administration::invoice_is_valid(&active_invoice) == A_ERR_SUCCESS; if (!can_save) ImGui::BeginDisabled(); ImGui::SameLine(); if (ImGui::Button(locale::get("form.save"), true)) { administration_writer::set_write_completed_event_callback(_reset_to_invoice_view); administration::invoice_add(&active_invoice); _reload_activities(); } if (!can_save) ImGui::EndDisabled(); } ImGui::Spacing(); ImGui::Separator(3.0f); ImGui::Spacing(); float availableWidth = ImGui::GetContentRegionAvail().x; ImGui::BeginChild("##invoicePanel", ImVec2(availableWidth - sidepanel_width, 0), ImGuiChildFlags_None); draw_expense_form(&active_invoice); ImGui::EndChild(); } static void draw_expense_view() { if (ImGui::Button(locale::get("form.back"), true, false)) { _reset_to_default_view(); } ImGui::SameLine(); if (ImGui::Button(locale::get("form.change"))) { _set_active_invoice(administration::invoice_create_copy(&active_invoice)); // We create a copy because of billing item list pointers. current_view_state = ui::view_state::EDIT_EXISTING; } ImGui::Spacing(); ImGui::Separator(3.0f); ImGui::Spacing(); float availableWidth = ImGui::GetContentRegionAvail().x; ImGui::BeginChild("##invoicePanel", ImVec2(availableWidth - sidepanel_width, 0), ImGuiChildFlags_None); draw_expense_form(&active_invoice, true); ImGui::EndChild(); _draw_activity_sidepanel(); } 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"))) { _reset_to_default_view(); 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(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; } }