/* * 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 "administration.hpp" #include "administration_writer.hpp" static activity* activity_buffer = 0; static u32 activity_count; 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; 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_invoices() { memops::unalloc(invoice_items_buffer); memops::unalloc(activity_buffer); } void ui::setup_invoices() { activity_buffer = (activity*)memops::alloc(sizeof(activity) * 250); current_view_state = ui::view_state::LIST_ALL; _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); } void draw_invoice_items_form(invoice* invoice, bool outgoing) { billing_item* buffer = invoice_items_buffer; u32 invoice_items = administration::billing_item_get_all_for_invoice(invoice, buffer); if (ImGui::BeginTable("TableBillingItems", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) { ImGui::TableSetupColumn("##actions", ImGuiTableColumnFlags_WidthFixed, 20); ImGui::TableSetupColumn(locale::get("invoice.table.amount"), ImGuiTableColumnFlags_WidthFixed, 80); ImGui::TableSetupColumn(locale::get("invoice.table.description")); ImGui::TableSetupColumn(locale::get("invoice.table.price"), ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableSetupColumn(locale::get("invoice.table.discount"), ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableSetupColumn(locale::get("invoice.table.net"), ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableSetupColumn(locale::get("invoice.table.tax%"), ImGuiTableColumnFlags_WidthFixed, 120); ImGui::TableSetupColumn(locale::get("invoice.table.tax"), ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableSetupColumn(locale::get("invoice.table.total"), ImGuiTableColumnFlags_WidthFixed, 100); ImGui::TableHeadersRow(); for (u32 i = 0; i < invoice_items; i++) { billing_item item = buffer[i]; a_err valid = administration::billing_item_is_valid(item); ImGui::TableNextRow(); ImGui::PushID(i); ImGui::TableSetColumnIndex(0); if (ImGui::Button("X")) { administration::billing_item_remove_from_invoice(invoice, item); } ImGui::TableSetColumnIndex(1); ImGui::InputFloat("##amount", &item.amount, 0.0f, 0.0f, "%.0f"); ImGui::SameLine(); ImGui::ToggleDropdown(&item.amount_is_percentage, "X", "%"); ImGui::TableSetColumnIndex(2); ImGui::PushItemWidth(-1); ImGui::InputTextWithError("##desc", item.description, IM_ARRAYSIZE(item.description), valid & A_ERR_MISSING_DESCRIPTION); ImGui::PopItemWidth(); ImGui::TableSetColumnIndex(3); ImGui::PushItemWidth(-1); ImGui::InputFloat("##price", &item.net_per_item, 0.0f, 0.0f, "%.2f"); ImGui::PopItemWidth(); ImGui::TableSetColumnIndex(4); ImGui::InputFloat("##discount", &item.discount, 0.0f, 0.0f, "%.2f"); ImGui::SameLine(); ImGui::ToggleDropdown(&item.discount_is_percentage, item.currency, "%"); ImGui::TableSetColumnIndex(5); ImGui::Text("%.2f %s", item.net, item.currency); ImGui::TableSetColumnIndex(6); ImGui::PushItemWidth(-1); ImGui::TaxRateDropdown(item.tax_internal_code, outgoing, valid & A_ERR_MISSING_TAX_RATE); ImGui::PopItemWidth(); ImGui::TableSetColumnIndex(7); ImGui::Text("%.2f %s", item.tax, item.currency); ImGui::TableSetColumnIndex(8); ImGui::Text("%.2f %s", item.total, item.currency); ImGui::PopID(); administration::billing_item_update_in_invoice(invoice, item); } ImGui::TableNextRow(); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(70, 70, 70, 255)); ImGui::TableSetColumnIndex(5); ImGui::Text("%.2f %s", invoice->orig_net, invoice->currency); ImGui::TableSetColumnIndex(7); ImGui::Text("%.2f %s", invoice->orig_tax, invoice->currency); ImGui::TableSetColumnIndex(8); ImGui::Text("%.2f %s", invoice->orig_total, invoice->currency); if (!strops::equals(invoice->currency, administration::get_default_currency())) { ImGui::TableNextRow(); ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, IM_COL32(50, 50, 50, 255)); ImGui::TableSetColumnIndex(2); ImGui::Text("%s %s", locale::get("invoice.form.finalSettlement"), administration::get_default_currency()); ImGui::TableSetColumnIndex(4); ImGui::InputFloat("##final_allowance", &invoice->allowance, 0.0f, 0.0f, "%.2f"); ImGui::SameLine(); ImGui::Text("%s", administration::get_default_currency()); ImGui::TableSetColumnIndex(5); ImGui::InputFloat("##final_net", &invoice->net, 0.0f, 0.0f, "%.2f"); ImGui::SameLine(); ImGui::Text("%s", administration::get_default_currency()); ImGui::TableSetColumnIndex(7); ImGui::InputFloat("##final_tax", &invoice->tax, 0.0f, 0.0f, "%.2f"); ImGui::SameLine(); ImGui::Text("%s", administration::get_default_currency()); ImGui::TableSetColumnIndex(8); ImGui::InputFloat("##final_total", &invoice->total, 0.0f, 0.0f, "%.2f"); ImGui::SameLine(); ImGui::Text("%s", administration::get_default_currency()); } ImGui::EndTable(); } } static void draw_invoice_form(invoice* buffer, bool viewing_only = false) { ImGui::BeginDisabled(); ImGui::Text("%s: %s", locale::get("invoice.form.invoicenumber"), buffer->sequential_number); ImGui::Text("%s: %s", locale::get("invoice.form.supplier"), buffer->supplier.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")); if (!viewing_only) ImGui::EndDisabled(); 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::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::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, true); if (viewing_only) ImGui::EndDisabled(); } static void draw_invoices_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_outgoing(current_page, items_per_page, invoice_list); u32 total_invoice_count = administration::invoice_get_outgoing_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 button and pagination. if (ImGui::Button(locale::get("form.create"))) { current_view_state = ui::view_state::CREATE; _set_active_invoice(administration::invoice_create_empty()); active_invoice.supplier = administration::company_info_get(); active_invoice.is_outgoing = 1; active_invoice.status = invoice_status::INVOICE_CONCEPT; } 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, 130); ImGui::TableSetupColumn(locale::get("invoice.table.customer")); ImGui::TableSetupColumn(locale::get("invoice.table.addressee")); 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.customer.name); ImGui::TableSetColumnIndex(2); ImGui::Text(c.addressee.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::SameLine(); } ImGui::EndTable(); } } static void _reset_to_invoice_view() { current_view_state = ui::view_state::VIEW_EXISTING; } static void _reset_to_default_view() { current_view_state = ui::view_state::LIST_ALL; ui::destroy_invoices(); ui::setup_invoices(); activity_count = 0; } 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_invoice_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_invoice_form(&active_invoice); ImGui::EndChild(); _draw_activity_sidepanel(); } static void draw_invoice_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_invoice_form(&active_invoice); ImGui::EndChild(); } static void draw_invoice_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::SameLine(); ImGui::PushItemWidth(100.0f); if (ImGui::BeginCombo("##Send", locale::get("ui.sendAs"))) { if (ImGui::Selectable(locale::get("ui.sendAs.email"), false)) { } if (ImGui::Selectable(locale::get("ui.sendAs.einvoice"), false)) { } ImGui::EndCombo(); } ImGui::PushItemWidth(0.0f); ImGui::SameLine(); ImGui::PushItemWidth(100.0f); if (ImGui::BeginCombo("##Export as", locale::get("ui.exportAs"))) { if (ImGui::Selectable("PDF", false)) { } if (ImGui::Selectable("UBL", false)) { } ImGui::EndCombo(); } ImGui::PushItemWidth(0.0f); ImGui::Spacing(); ImGui::Separator(3.0f); ImGui::Spacing(); float availableWidth = ImGui::GetContentRegionAvail().x; ImGui::BeginChild("##invoicePanel", ImVec2(availableWidth - sidepanel_width, 0), ImGuiChildFlags_None); draw_invoice_form(&active_invoice, true); ImGui::EndChild(); _draw_activity_sidepanel(); } void ui::draw_invoices() { switch(current_view_state) { case ui::view_state::LIST_ALL: draw_invoices_list(); break; case ui::view_state::CREATE: draw_invoice_create(); break; case ui::view_state::EDIT_EXISTING: draw_invoice_update(); break; case ui::view_state::VIEW_EXISTING: draw_invoice_view(); break; } }