From 572caa74ed824fefa02eb81adc7639a783f243c7 Mon Sep 17 00:00:00 2001 From: Aldrik Ramaekers Date: Sun, 10 Aug 2025 19:03:46 +0200 Subject: working on invoice form --- docs/README.rst | 5 +- include/administration.hpp | 4 +- include/strops.hpp | 3 +- include/ui.hpp | 2 +- libs/ImGuiDatePicker/ImGuiDatePicker.cpp | 393 +++++++++++++++++++++++ libs/ImGuiDatePicker/ImGuiDatePicker.hpp | 19 ++ libs/ImGuiDatePicker/Images/AltFontDemoImage.png | Bin 0 -> 25829 bytes libs/ImGuiDatePicker/Images/DemoGif.gif | Bin 0 -> 473676 bytes libs/ImGuiDatePicker/Images/DemoImage.png | Bin 0 -> 23349 bytes libs/ImGuiDatePicker/LICENSE | 21 ++ libs/ImGuiDatePicker/README.md | 77 +++++ run.bat | 4 +- src/administration.cpp | 106 ++++++ src/strops.cpp | 17 + src/ui/helpers.cpp | 14 +- src/ui/ui_contacts.cpp | 31 +- src/ui/ui_invoices.cpp | 81 +++-- src/ui/ui_projects.cpp | 2 +- 18 files changed, 738 insertions(+), 41 deletions(-) create mode 100644 libs/ImGuiDatePicker/ImGuiDatePicker.cpp create mode 100644 libs/ImGuiDatePicker/ImGuiDatePicker.hpp create mode 100644 libs/ImGuiDatePicker/Images/AltFontDemoImage.png create mode 100644 libs/ImGuiDatePicker/Images/DemoGif.gif create mode 100644 libs/ImGuiDatePicker/Images/DemoImage.png create mode 100644 libs/ImGuiDatePicker/LICENSE create mode 100644 libs/ImGuiDatePicker/README.md diff --git a/docs/README.rst b/docs/README.rst index 7dedc8d..4affde6 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -36,5 +36,6 @@ This section lists all country specific implementations to be compliant. If the 3. Dependencies --------------- -- ImGui 1.92.1 -- simclist 1.5 \ No newline at end of file +- ImGui 1.92.1 (https://github.com/ocornut/imgui) +- simclist 1.5 (https://mij.oltrelinux.com/devel/simclist/) +- ImGuiDatePicker (https://github.com/DnA-IntRicate/ImGuiDatePicker) \ No newline at end of file diff --git a/include/administration.hpp b/include/administration.hpp index 68deb78..0917ee5 100644 --- a/include/administration.hpp +++ b/include/administration.hpp @@ -95,8 +95,8 @@ typedef struct char customer_id[16]; char supplier_id[16]; time_t issued_at; - time_t delivered_at; time_t expires_at; + time_t delivered_at; char document[255]; list_t billing_items; char project_id[16]; @@ -147,6 +147,7 @@ u32 administration_get_contact_count(); u32 administration_get_contacts(u32 page_index, u32 page_size, contact* buffer); contact administration_create_empty_contact(); bool administration_is_contact_valid(contact data); +int administration_get_contact_recommendations(contact* buffer, int buf_size, char* name); void administration_cancel_project(project data); bool administration_remove_project(project data); @@ -155,6 +156,7 @@ bool administration_update_project(project data); char* administration_project_get_status_string(project data); u32 administration_get_project_count(); u32 administration_get_projects(u32 page_index, u32 page_size, project* buffer); +bool administration_is_project_valid(project data); project administration_create_empty_project(); u32 administration_get_tax_bracket_count(); diff --git a/include/strops.hpp b/include/strops.hpp index daa1d71..08dbd5a 100644 --- a/include/strops.hpp +++ b/include/strops.hpp @@ -1,3 +1,4 @@ #pragma once -size_t strops_copy(char *dst, const char *src, size_t size); \ No newline at end of file +size_t strops_copy(char *dst, const char *src, size_t size); +char* strops_stristr(char* a, char* b); \ No newline at end of file diff --git a/include/ui.hpp b/include/ui.hpp index 7a02329..5c5b6fc 100644 --- a/include/ui.hpp +++ b/include/ui.hpp @@ -11,7 +11,7 @@ typedef enum void ui_helper_draw_required_tag(); void ui_helper_show_toast(const char* msg); void ui_helper_draw_toasts(); -void ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size, char** suggestions, int suggestion_count); +int ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size, char** suggestions, int suggestion_count); void ui_draw_main(); void ui_draw_contacts(); diff --git a/libs/ImGuiDatePicker/ImGuiDatePicker.cpp b/libs/ImGuiDatePicker/ImGuiDatePicker.cpp new file mode 100644 index 0000000..8c81b2b --- /dev/null +++ b/libs/ImGuiDatePicker/ImGuiDatePicker.cpp @@ -0,0 +1,393 @@ +#include "ImGuiDatePicker.hpp" +#include +#include +#include +#include +#include + + +#define GET_DAY(timePoint) int(timePoint.tm_mday) +#define GET_MONTH_UNSCALED(timePoint) timePoint.tm_mon +#define GET_MONTH(timePoint) int(GET_MONTH_UNSCALED(timePoint) + 1) +#define GET_YEAR(timePoint) int(timePoint.tm_year + 1900) + +#define SET_DAY(timePoint, day) timePoint.tm_mday = day +#define SET_MONTH(timePoint, month) timePoint.tm_mon = month - 1 +#define SET_YEAR(timePoint, year) timePoint.tm_year = year - 1900 + +namespace ImGui +{ + static const std::vector MONTHS = + { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + }; + + static const std::vector DAYS = + { + "Mo", + "Tu", + "We", + "Th", + "Fr", + "Sa", + "Su" + }; + + // Implements Zeller's Congruence to determine the day of week [1, 7](Mon-Sun) from the given parameters + inline static int DayOfWeek(int dayOfMonth, int month, int year) noexcept + { + if ((month == 1) || (month == 2)) + { + month += 12; + year -= 1; + } + + int h = (dayOfMonth + + static_cast(std::floor((13 * (month + 1)) / 5.0)) + + year + + static_cast(std::floor(year / 4.0)) + - static_cast(std::floor(year / 100.0)) + + static_cast(std::floor(year / 400.0))) % 7; + + return static_cast(std::floor(((h + 5) % 7) + 1)); + } + + constexpr static bool IsLeapYear(int year) noexcept + { + if ((year % 400) == 0) + return true; + + if ((year % 4 == 0) && ((year % 100) != 0)) + return true; + + return false; + } + + inline static int NumDaysInMonth(int month, int year) + { + if (month == 2) + return IsLeapYear(year) ? 29 : 28; + + // Month index paired to the number of days in that month excluding February + static const std::unordered_map monthDayMap = + { + { 1, 31 }, + { 3, 31 }, + { 4, 30 }, + { 5, 31 }, + { 6, 30 }, + { 7, 31 }, + { 8, 31 }, + { 9, 30 }, + { 10, 31 }, + { 11, 30 }, + { 12, 31 } + }; + + return monthDayMap.at(month); + } + + // Returns the number of calendar weeks spanned by month in the specified year + inline static int NumWeeksInMonth(int month, int year) + { + int days = NumDaysInMonth(month, year); + int firstDay = DayOfWeek(1, month, year); + + return static_cast(std::ceil((days + firstDay - 1) / 7.0)); + } + + // Returns a vector containing dates as they would appear on the calendar for a given week. Populates 0 if there is no day. + inline static std::vector CalendarWeek(int week, int startDay, int daysInMonth) + { + std::vector res(7, 0); + int startOfWeek = 7 * (week - 1) + 2 - startDay; + + if (startOfWeek >= 1) + res[0] = startOfWeek; + + for (int i = 1; i < 7; ++i) + { + int day = startOfWeek + i; + if ((day >= 1) && (day <= daysInMonth)) + res[i] = day; + } + + return res; + } + + constexpr static tm EncodeTimePoint(int dayOfMonth, int month, int year) noexcept + { + tm res{ }; + res.tm_isdst = -1; + SET_DAY(res, dayOfMonth); + SET_MONTH(res, month); + SET_YEAR(res, year); + + return res; + } + + inline static std::string TimePointToLongString(const tm& timePoint) noexcept + { + std::string day = std::to_string(GET_DAY(timePoint)); + std::string month = MONTHS[GET_MONTH_UNSCALED(timePoint)]; + std::string year = std::to_string(GET_YEAR(timePoint)); + + return std::string(day + " " + month + " " + year); + } + + inline static tm Today() noexcept + { + std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); + std::time_t currentTime = std::chrono::system_clock::to_time_t(now); + + tm res; + gmtime_s(&res, ¤tTime); + + return res; + } + + inline static tm PreviousMonth(const tm& timePoint) noexcept + { + int month = GET_MONTH(timePoint); + int year = GET_YEAR(timePoint); + + if (month == 1) + { + int newDay = std::min(GET_DAY(timePoint), NumDaysInMonth(12, --year)); + return EncodeTimePoint(newDay, 12, year); + } + + int newDay = std::min(GET_DAY(timePoint), NumDaysInMonth(--month, year)); + return EncodeTimePoint(newDay, month, year); + } + + inline static tm NextMonth(const tm& timePoint) noexcept + { + int month = GET_MONTH(timePoint); + int year = GET_YEAR(timePoint); + + if (month == 12) + { + int newDay = std::min(GET_DAY(timePoint), NumDaysInMonth(1, ++year)); + return EncodeTimePoint(newDay, 1, year); + } + + int newDay = std::min(GET_DAY(timePoint), NumDaysInMonth(++month, year)); + return EncodeTimePoint(newDay, month, year); + } + + constexpr static bool IsMinDate(const tm& timePoint) noexcept + { + return (GET_MONTH(timePoint) == 1) && (GET_YEAR(timePoint) == IMGUI_DATEPICKER_YEAR_MIN); + } + + constexpr static bool IsMaxDate(const tm& timePoint) noexcept + { + return (GET_MONTH(timePoint) == 12) && (GET_YEAR(timePoint) == IMGUI_DATEPICKER_YEAR_MAX); + } + + static bool ComboBox(const std::string& label, const std::vector& items, int& v, ImFont* altFont) + { + bool res = false; + + ImGui::PushFont(altFont); + if (ImGui::BeginCombo(label.c_str(), items[v].c_str())) + { + for (int i = 0; i < items.size(); ++i) + { + bool selected = (items[v] == items[i]); + if (ImGui::Selectable(items[i].c_str(), &selected)) + { + v = i; + res = true; + } + + if (selected) + ImGui::SetItemDefaultFocus(); + } + + ImGui::EndCombo(); + } + + ImGui::PopFont(); + return res; + } + + bool DatePickerEx(const std::string& label, tm& v, ImFont* altFont, bool clampToBorder, float itemSpacing) + { + bool res = false; + + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + bool hiddenLabel = label.substr(0, 2) == "##"; + std::string myLabel = (hiddenLabel) ? label.substr(2) : label; + + if (!hiddenLabel) + { + Text("%s", label.c_str()); + SameLine((itemSpacing == 0.0f) ? 0.0f : GetCursorPos().x + itemSpacing); + } + + if (clampToBorder) + SetNextItemWidth(GetContentRegionAvail().x); + + const ImVec2 windowSize = ImVec2(274.5f, 301.5f); + SetNextWindowSize(windowSize); + + if (BeginCombo(std::string("##" + myLabel).c_str(), TimePointToLongString(v).c_str())) + { + int monthIdx = GET_MONTH_UNSCALED(v); + int year = GET_YEAR(v); + + PushItemWidth((GetContentRegionAvail().x * 0.5f)); + + if (ComboBox("##CmbMonth_" + myLabel, MONTHS, monthIdx, altFont)) + { + SET_MONTH(v, monthIdx + 1); + res = true; + } + + PopItemWidth(); + SameLine(); + PushItemWidth(GetContentRegionAvail().x); + + if (InputInt(std::string("##IntYear_" + myLabel).c_str(), &year)) + { + SET_YEAR(v, std::min(std::max(IMGUI_DATEPICKER_YEAR_MIN, year), IMGUI_DATEPICKER_YEAR_MAX)); + res = true; + } + + PopItemWidth(); + + const float contentWidth = GetContentRegionAvail().x; + const float arrowSize = GetFrameHeight(); + const float arrowButtonWidth = arrowSize * 2.0f + GetStyle().ItemSpacing.x; + const float bulletSize = arrowSize - 5.0f; + const float bulletButtonWidth = bulletSize + GetStyle().ItemSpacing.x; + const float combinedWidth = arrowButtonWidth + bulletButtonWidth; + const float offset = (contentWidth - combinedWidth) * 0.5f; + + SetCursorPosX(GetCursorPosX() + offset); + PushStyleVar(ImGuiStyleVar_FrameRounding, 20.0f); + PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + BeginDisabled(IsMinDate(v)); + + if (ArrowButtonEx(std::string("##ArrowLeft_" + myLabel).c_str(), ImGuiDir_Left, ImVec2(arrowSize, arrowSize))) + { + v = PreviousMonth(v); + res = true; + } + + EndDisabled(); + PopStyleColor(2); + SameLine(); + PushStyleColor(ImGuiCol_Button, GetStyleColorVec4(ImGuiCol_Text)); + SetCursorPosY(GetCursorPosY() + 2.0f); + + if (ButtonEx(std::string("##ArrowMid_" + myLabel).c_str(), ImVec2(bulletSize, bulletSize))) + { + v = Today(); + res = true; + CloseCurrentPopup(); + } + + PopStyleColor(); + SameLine(); + PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + BeginDisabled(IsMaxDate(v)); + + if (ArrowButtonEx(std::string("##ArrowRight_" + myLabel).c_str(), ImGuiDir_Right, ImVec2(arrowSize, arrowSize))) + { + v = NextMonth(v); + res = true; + } + + EndDisabled(); + PopStyleColor(2); + PopStyleVar(); + + constexpr ImGuiTableFlags TABLE_FLAGS = ImGuiTableFlags_BordersOuter | ImGuiTableFlags_SizingFixedFit | + ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_NoHostExtendY; + + if (BeginTable(std::string("##Table_" + myLabel).c_str(), 7, TABLE_FLAGS, GetContentRegionAvail())) + { + for (const auto& day : DAYS) + TableSetupColumn(day.c_str(), ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderWidth, 30.0f); + + PushStyleColor(ImGuiCol_HeaderHovered, GetStyleColorVec4(ImGuiCol_TableHeaderBg)); + PushStyleColor(ImGuiCol_HeaderActive, GetStyleColorVec4(ImGuiCol_TableHeaderBg)); + PushFont(altFont); + TableHeadersRow(); + PopStyleColor(2); + PopFont(); + + TableNextRow(); + TableSetColumnIndex(0); + + int month = monthIdx + 1; + int firstDayOfMonth = DayOfWeek(1, month, year); + int numDaysInMonth = NumDaysInMonth(month, year); + int numWeeksInMonth = NumWeeksInMonth(month, year); + + for (int i = 1; i <= numWeeksInMonth; ++i) + { + for (const auto& day : CalendarWeek(i, firstDayOfMonth, numDaysInMonth)) + { + if (day != 0) + { + PushStyleVar(ImGuiStyleVar_FrameRounding, 20.0f); + + const bool selected = day == GET_DAY(v); + if (!selected) + { + PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); + } + + if (Button(std::to_string(day).c_str(), ImVec2(GetContentRegionAvail().x, GetTextLineHeightWithSpacing() + 5.0f))) + { + v = EncodeTimePoint(day, month, year); + res = true; + CloseCurrentPopup(); + } + + if (!selected) + PopStyleColor(2); + + PopStyleVar(); + } + + if (day != numDaysInMonth) + TableNextColumn(); + } + } + + EndTable(); + } + + EndCombo(); + } + + return res; + } + + bool DatePicker(const std::string& label, tm& v, bool clampToBorder, float itemSpacing) + { + return DatePickerEx(label, v, nullptr, clampToBorder, itemSpacing); + } +} diff --git a/libs/ImGuiDatePicker/ImGuiDatePicker.hpp b/libs/ImGuiDatePicker/ImGuiDatePicker.hpp new file mode 100644 index 0000000..91ed569 --- /dev/null +++ b/libs/ImGuiDatePicker/ImGuiDatePicker.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include + + +#ifndef IMGUI_DATEPICKER_YEAR_MIN + #define IMGUI_DATEPICKER_YEAR_MIN 1900 +#endif // !IMGUI_DATEPICKER_YEAR_MIN + +#ifndef IMGUI_DATEPICKER_YEAR_MAX + #define IMGUI_DATEPICKER_YEAR_MAX 3000 +#endif // !IMGUI_DATEPICKER_YEAR_MAX + +namespace ImGui +{ + IMGUI_API bool DatePickerEx(const std::string& label, tm& v, ImFont* altFont, bool clampToBorder = false, float itemSpacing = 130.0f); + + IMGUI_API bool DatePicker(const std::string& label, tm& v, bool clampToBorder = false, float itemSpacing = 130.0f); +} diff --git a/libs/ImGuiDatePicker/Images/AltFontDemoImage.png b/libs/ImGuiDatePicker/Images/AltFontDemoImage.png new file mode 100644 index 0000000..ee12d8d Binary files /dev/null and b/libs/ImGuiDatePicker/Images/AltFontDemoImage.png differ diff --git a/libs/ImGuiDatePicker/Images/DemoGif.gif b/libs/ImGuiDatePicker/Images/DemoGif.gif new file mode 100644 index 0000000..7e54364 Binary files /dev/null and b/libs/ImGuiDatePicker/Images/DemoGif.gif differ diff --git a/libs/ImGuiDatePicker/Images/DemoImage.png b/libs/ImGuiDatePicker/Images/DemoImage.png new file mode 100644 index 0000000..0cd03dc Binary files /dev/null and b/libs/ImGuiDatePicker/Images/DemoImage.png differ diff --git a/libs/ImGuiDatePicker/LICENSE b/libs/ImGuiDatePicker/LICENSE new file mode 100644 index 0000000..66090be --- /dev/null +++ b/libs/ImGuiDatePicker/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Adam Foflonker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/libs/ImGuiDatePicker/README.md b/libs/ImGuiDatePicker/README.md new file mode 100644 index 0000000..3da59ab --- /dev/null +++ b/libs/ImGuiDatePicker/README.md @@ -0,0 +1,77 @@ +# ImGuiDatePicker +ImGuiDatePicker is a custom calendar-style date-picker widget for [Dear ImGui](https://github.com/ocornut/imgui) written in `C++20` that can easily be compiled alongside `ImGui` by adding the header and source files to your `ImGui` project. + +![DemoGif](Images/DemoGif.gif) +Font used: [Open Sans](https://fonts.google.com/specimen/Open+Sans) + +ImGuiDatePicker uses the `tm` struct for working with dates: +``` C++ +struct tm +{ + int tm_sec; // seconds after the minute - [0, 60] including leap second + int tm_min; // minutes after the hour - [0, 59] + int tm_hour; // hours since midnight - [0, 23] + int tm_mday; // day of the month - [1, 31] + int tm_mon; // months since January - [0, 11] + int tm_year; // years since 1900 + int tm_wday; // days since Sunday - [0, 6] + int tm_yday; // days since January 1 - [0, 365] + int tm_isdst; // daylight savings time flag +}; +``` +## Including +`IMGUI_DATEPICKER_YEAR_MIN` and `IMGUI_DATEPICKER_YEAR_MAX` should be defined before including [ImGuiDatePicker.hpp](ImGuiDatePicker.hpp). If they are not defined, they will default to `1900` and `3000` respectively. +``` C++ +// Define the lowest year that the picker can select. In this example, '1970' is the Unix epoch. +#define IMGUI_DATEPICKER_YEAR_MIN 1970 +// Define the highest year that the picker can select. +#define IMGUI_DATEPICKER_YEAR_MAX 3000 +#include +``` +## Usage +``` C++ +// Get today's date and store it in a 'tm' struct named 't' +std::chrono::system_clock::time_point now = std::chrono::system_clock::now(); +std::time_t currentTime = std::chrono::system_clock::to_time_t(now); +tm t = *std::gmtime(¤tTime); + +// Use the picker +if (ImGui::DatePicker("Date", t)) +{ + // Perform some event whenever the date 't' is changed +} +``` +An `alt font` can also be supplied to the picker by calling `DatePickerEx`. This `alt font` will be used for the picker's days-of-the-week headers and for the month-picker combo box. +``` C++ +ImFont* boldFont = GetBoldFontFromSomewhere(); +tm t = GetTmFromSomewhere(); +ImGui::DatePickerEx("Date", t, boldFont); +``` +![AltFontDemoImage](Images/AltFontDemoImage.png) + +## License +ImGuiDatePicker is licensed under the MIT License. See [LICENSE](LICENSE). + +``` +MIT License + +Copyright (c) 2024 Adam Foflonker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/run.bat b/run.bat index 7197b94..f557bb4 100644 --- a/run.bat +++ b/run.bat @@ -17,11 +17,11 @@ if not exist "%VCVARSALL%" ( call "%VCVARSALL%" x64 @set OUT_DIR=build\\ @set OUT_EXE=accounting -@set LIB_SOURCES=libs\imgui-1.92.1\backends\imgui_impl_dx11.cpp libs\imgui-1.92.1\backends\imgui_impl_win32.cpp libs\imgui-1.92.1\imgui*.cpp libs\simclist-1.5\simclist.c +@set LIB_SOURCES=libs\imgui-1.92.1\backends\imgui_impl_dx11.cpp libs\imgui-1.92.1\backends\imgui_impl_win32.cpp libs\imgui-1.92.1\imgui*.cpp libs\simclist-1.5\simclist.c libs\ImGuiDatePicker\*.cpp @set SOURCES= src\*.cpp src\ui\*.cpp src\locales\*.cpp @set LIBS=opengl32.lib Advapi32.lib Shell32.lib Ole32.lib User32.lib Pathcch.lib D3D11.lib @set FLAGS=/nologo /Ob0 /MT /Oy- /Zi /FS /W4 /EHsc /utf-8 -@set INCLUDE_DIRS=/I"libs/imgui-1.92.1" /I"libs/imgui-1.92.1/backends" /I"/" /I"libs/simclist-1.5" /Iinclude +@set INCLUDE_DIRS=/I"libs/imgui-1.92.1" /I"libs/imgui-1.92.1/backends" /I"/" /I"libs/simclist-1.5" /I"libs/" /Iinclude cl %FLAGS% %INCLUDE_DIRS% %SOURCES% %LIB_SOURCES% /Fe%OUT_DIR%/%OUT_EXE%.exe /Fd%OUT_DIR%/vc140.pdb /Fo%OUT_DIR%/ /link %LIBS% if "%1"=="-r" call "%OUT_DIR%/%OUT_EXE%.exe" diff --git a/src/administration.cpp b/src/administration.cpp index 869855e..a34ece8 100644 --- a/src/administration.cpp +++ b/src/administration.cpp @@ -199,6 +199,71 @@ static void administration_create_default_cost_centers() ADD_COSTCENTER("costcenter.other_specialized", "OTHR"); } +static void administration_create_debug_data() +{ + #define ADD_CONSUMER(_name, _addr1, _addr2, _cc)\ + {contact _c = administration_create_empty_contact();\ + strops_copy(_c.name, _name, sizeof(_c.name));\ + strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ + strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ + strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ + _c.type = contact_type::CONTACT_CONSUMER;\ + administration_create_contact(_c);}; + + #define ADD_BUSINESS(_name, _addr1, _addr2, _cc, _tc, _bc)\ + {contact _c = administration_create_empty_contact();\ + strops_copy(_c.name, _name, sizeof(_c.name));\ + strops_copy(_c.address.address1, _addr1, sizeof(_c.address.address1));\ + strops_copy(_c.address.address2, _addr2, sizeof(_c.address.address2));\ + strops_copy(_c.address.country_code, _cc, sizeof(_c.address.country_code));\ + strops_copy(_c.taxid, _tc, sizeof(_c.taxid));\ + strops_copy(_c.businessid, _bc, sizeof(_c.businessid));\ + _c.type = contact_type::CONTACT_BUSINESS;\ + administration_create_contact(_c);}; + + #define ADD_PROJECT(_name)\ + {project _c = administration_create_empty_project();\ + strops_copy(_c.description, _name, sizeof(_c.description));\ + administration_create_project(_c);}; + + ADD_CONSUMER("Emma Müller", "Hauptstraße 12", "10115 Berlin", "DE"); + ADD_CONSUMER("Luca Rossi", "Via Roma 45", "00184 Roma", "IT"); + ADD_CONSUMER("Sofia Garcia", "Calle Mayor 7", "28013 Madrid", "ES"); + ADD_CONSUMER("Jean Dupont", "10 Rue de la Paix", "75002 Paris", "FR"); + ADD_CONSUMER("Anna Nowak", "ul. Kwiatowa 3", "00-001 Warszawa", "PL"); + ADD_CONSUMER("Mikkel Jensen", "Østergade 8", "8000 Aarhus", "DK"); + ADD_CONSUMER("Maria Svensson", "Kungsgatan 15", "111 22 Stockholm", "SE"); + ADD_CONSUMER("Péter Kovács", "Fő utca 25", "1051 Budapest", "HU"); + ADD_CONSUMER("Lucas Silva", "Rua Augusta 100", "1250-001 Lisboa", "PT"); + ADD_CONSUMER("Isabelle Lefevre", "5 Place Stanislas", "54000 Nancy", "FR"); + + ADD_BUSINESS("Schmidt & Co GmbH", "Friedrichstraße 45", "10117 Berlin", "DE", "DE123456789", "HRB123456"); + ADD_BUSINESS("Bianchi Srl", "Corso Venezia 12", "20121 Milano", "IT", "IT987654321", "MI1234567"); + ADD_BUSINESS("Fernández y Asociados", "Gran Vía 20", "28013 Madrid", "ES", "ES456789123", "CIFB123456"); + ADD_BUSINESS("Martin & Partners", "12 Avenue Victor Hugo", "75016 Paris", "FR", "FR321654987", "SIRET123456"); + ADD_BUSINESS("Zielińska Consulting", "ul. Marszałkowska 10", "00-590 Warszawa", "PL", "PL789123456", "REGON123456"); + ADD_BUSINESS("Sørensen ApS", "Strøget 3", "1460 København", "DK", "DK654321789", "CVR12345678"); + ADD_BUSINESS("Johansson AB", "Drottninggatan 22", "111 51 Stockholm", "SE", "SE987654321", "OrgNr1234567"); + ADD_BUSINESS("Nagy Kft.", "Andrássy út 60", "1062 Budapest", "HU", "HU123987654", "Cégjegyzékszám123"); + ADD_BUSINESS("Santos Lda.", "Avenida da Liberdade 50", "1250-142 Lisboa", "PT", "PT321789654", "NIPC123456789"); + ADD_BUSINESS("Dupuis SARL", "8 Rue Saint-Denis", "75001 Paris", "FR", "FR456123789", "SIREN123456"); + ADD_BUSINESS("Müller & Söhne GmbH", "Leipziger Platz 8", "10117 Berlin", "DE", "DE654987321", "HRB987654"); + ADD_BUSINESS("Romano Srl", "Via Garibaldi 14", "16124 Genova", "IT", "IT321654987", "GE1239876"); + ADD_BUSINESS("López Asociados", "Plaza del Pilar 6", "50003 Zaragoza", "ES", "ES789321654", "CIFC654321"); + ADD_BUSINESS("Laurent & Fils", "15 Boulevard Haussmann", "75009 Paris", "FR", "FR987321654", "SIRET654987"); + ADD_BUSINESS("Kowalczyk Sp. z o.o.", "ul. Piotrkowska 55", "90-001 Łódź", "PL", "PL123456789", "REGON654321"); + ADD_BUSINESS("Nielsen ApS", "Nørregade 12", "1165 København", "DK", "DK789456123", "CVR87654321"); + ADD_BUSINESS("Lindberg AB", "Vasagatan 18", "111 20 Stockholm", "SE", "SE456789123", "OrgNr7654321"); + ADD_BUSINESS("Szabó Kft.", "Kossuth Lajos tér 1", "1055 Budapest", "HU", "HU987123654", "Cégjegyzékszám654321"); + ADD_BUSINESS("Costa Lda.", "Rua do Ouro 24", "1100-063 Lisboa", "PT", "PT654123987", "NIPC987654321"); + ADD_BUSINESS("Moreau SARL", "3 Place de la République", "75011 Paris", "FR", "FR321456987", "SIREN789123"); + + ADD_PROJECT("eCommerce"); + ADD_PROJECT("Retail store #1"); + ADD_PROJECT("Retail store #2"); + ADD_PROJECT("Kayak rental"); +} + void administration_create() { g_administration.next_id = 1; @@ -220,6 +285,7 @@ void administration_create() administration_create_default_tax_brackets(); administration_create_default_cost_centers(); + administration_create_debug_data(); } void administration_destroy() @@ -232,6 +298,8 @@ void administration_destroy() bool administration_create_contact(contact data) { + if (!administration_is_contact_valid(data)) return false; + contact* new_contact = (contact*)malloc(sizeof(contact)); memcpy((void*)new_contact, (void*)&data, sizeof(contact)); list_append(&g_administration.contacts, new_contact); @@ -250,6 +318,8 @@ bool administration_can_contact_be_deleted(contact data) bool administration_update_contact(contact data) { + if (!administration_is_contact_valid(data)) return false; + list_iterator_start(&g_administration.contacts); while (list_iterator_hasnext(&g_administration.contacts)) { contact* c = (contact *)list_iterator_next(&g_administration.contacts); @@ -309,6 +379,25 @@ u32 administration_get_contacts(u32 page_index, u32 page_size, contact* buffer) return write_cursor; } +int administration_get_contact_recommendations(contact* buffer, int buf_size, char* name) +{ + int write_cursor = 0; + if (name[0] == '\0') return 0; + + list_iterator_start(&g_administration.contacts); + while (list_iterator_hasnext(&g_administration.contacts)) { + contact c = *(contact *)list_iterator_next(&g_administration.contacts); + + if (strops_stristr(c.name, name)) { + buffer[write_cursor++] = c; + if (write_cursor >= buf_size) break; + } + } + list_iterator_stop(&g_administration.contacts); + + return write_cursor; +} + char* administration_get_file_path() { return g_administration.path; @@ -348,6 +437,11 @@ void administration_cancel_project(project data) administration_update_project(data); } +bool administration_is_project_valid(project data) +{ + return strlen(data.description) > 0; +} + char* administration_project_get_status_string(project data) { switch(data.state) @@ -362,6 +456,8 @@ char* administration_project_get_status_string(project data) bool administration_create_project(project data) { + if (!administration_is_project_valid(data)) return false; + data.state = project_state::PROJECT_RUNNING; data.start_date = time(NULL); data.end_date = 0; @@ -376,6 +472,8 @@ bool administration_create_project(project data) bool administration_update_project(project data) { + if (!administration_is_project_valid(data)) return false; + list_iterator_start(&g_administration.projects); while (list_iterator_hasnext(&g_administration.projects)) { project* c = (project *)list_iterator_next(&g_administration.projects); @@ -572,12 +670,20 @@ static s32 administration_create_sequence_number() return g_administration.next_sequence_number; } +static time_t administration_get_default_invoice_expire_duration() +{ + return (30 * 24 * 60 * 60); // 30 days +} + invoice administration_create_empty_invoice() { invoice result; memset(&result, 0, sizeof(invoice)); snprintf(result.id, sizeof(result.id), "I/%d", administration_create_id()); snprintf(result.sequential_number, sizeof(result.id), "INV%010d", administration_create_sequence_number()); + result.issued_at = time(NULL); + result.delivered_at = time(NULL); + result.expires_at = time(NULL) + administration_get_default_invoice_expire_duration(); return result; } diff --git a/src/strops.cpp b/src/strops.cpp index 42042cb..116eec4 100644 --- a/src/strops.cpp +++ b/src/strops.cpp @@ -1,4 +1,5 @@ #include +#include #include "strops.hpp" @@ -15,4 +16,20 @@ size_t strops_copy(char *dst, const char *src, size_t size) dst[srclen] = '\0'; return (srclen); +} + +char* strops_stristr(char* haystack, char* needle) +{ + do { + const char* h = haystack; + const char* n = needle; + while (tolower((unsigned char) *h) == tolower((unsigned char ) *n) && *n) { + h++; + n++; + } + if (*n == 0) { + return (char *) haystack; + } + } while (*haystack++); + return 0; } \ No newline at end of file diff --git a/src/ui/helpers.cpp b/src/ui/helpers.cpp index a239f4c..4c6647e 100644 --- a/src/ui/helpers.cpp +++ b/src/ui/helpers.cpp @@ -65,20 +65,24 @@ void ui_helper_draw_required_tag() ImGui::PopStyleColor(); } -void ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size, +int ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, char* buffer, size_t buf_size, char* suggestions[], int suggestion_count) { + int result = -1; static bool is_open = false; ImGui::InputTextWithHint(label, hint, buffer, buf_size); + if (buffer[0] == '\0' && is_open) is_open = false; + if (suggestion_count == 0 && is_open) is_open = false; + bool is_active = ImGui::IsItemActive(); - if (is_active && buffer[0] != '\0') + if (is_active && buffer[0] != '\0' && suggestion_count > 0) { is_open = true; } if (is_open) { - ImGui::BeginChild("autocomplete_popup", ImVec2(0, 100), true); + ImGui::BeginChild("autocomplete_popup", ImVec2(0, 10.0f + suggestion_count*23.0f), true); { ImVec2 win_pos = ImGui::GetWindowPos(); ImVec2 win_size = ImGui::GetWindowSize(); @@ -97,7 +101,7 @@ void ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, ch // Copy selected suggestion to buffer strops_copy(buffer, suggestions[i], buf_size); buffer[buf_size - 1] = '\0'; - + result = i; is_open = false; } } @@ -105,4 +109,6 @@ void ui_helper_TextInputWithAutocomplete(const char* label, const char* hint, ch ImGui::EndChild(); } } + return result; } + diff --git a/src/ui/ui_contacts.cpp b/src/ui/ui_contacts.cpp index c381db5..d57776e 100644 --- a/src/ui/ui_contacts.cpp +++ b/src/ui/ui_contacts.cpp @@ -18,9 +18,8 @@ void ui_setup_contacts() memset(&selected_for_removal, 0, sizeof(contact)); } -void draw_contact_form(contact* buffer, bool viewing_only = false) +void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0) { - bool with_autocomplete = false; const char* selected_country = NULL; ImGui::Spacing(); @@ -37,12 +36,26 @@ void draw_contact_form(contact* buffer, bool viewing_only = false) // 2. Full name ImGui::SetNextItemWidth(widthAvailable*0.5f); if (with_autocomplete) { - contact autocomplete_list[5]; - int autocomplete_count = 5; - char* autocomplete_strings[5] = { "1", "2", "3", "4", "5" }; + contact autocomplete_list[5]; + int autocomplete_count = administration_get_contact_recommendations(autocomplete_list, 5, buffer->name); + char* autocomplete_strings[5]; + + for (int i = 0; i < autocomplete_count; i++) + { + autocomplete_strings[i] = autocomplete_list[i].name; + } - ui_helper_TextInputWithAutocomplete(localize("contact.form.fullname"), localize("contact.form.fullname"), + int autocomplete_index = ui_helper_TextInputWithAutocomplete(localize("contact.form.fullname"), localize("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name), (char**)autocomplete_strings, autocomplete_count); + + if (on_autocomplete) { + *on_autocomplete = autocomplete_index != -1; + } + + if (autocomplete_index != -1) + { + memcpy(buffer, &autocomplete_list[autocomplete_index], sizeof(contact)); + } } else ImGui::InputTextWithHint(localize("contact.form.fullname"), localize("contact.form.fullname"), buffer->name, IM_ARRAYSIZE(buffer->name)); ImGui::SameLine();ui_helper_draw_required_tag(); @@ -131,6 +144,12 @@ void draw_contact_form(contact* buffer, bool viewing_only = false) if (viewing_only) ImGui::EndDisabled(); } +void draw_contact_form(contact* buffer, bool viewing_only = false) +{ + draw_contact_form_ex(buffer, viewing_only, false, 0); +} + + static void draw_contact_list() { const u32 items_per_page = 50; diff --git a/src/ui/ui_invoices.cpp b/src/ui/ui_invoices.cpp index fe4f5a8..8ee099c 100644 --- a/src/ui/ui_invoices.cpp +++ b/src/ui/ui_invoices.cpp @@ -1,4 +1,7 @@ #include +#include + +#include "ImGuiDatePicker/ImGuiDatePicker.hpp" #include "strops.hpp" #include "ui.hpp" @@ -9,7 +12,7 @@ static view_state current_view_state = view_state::LIST; static invoice active_invoice; -extern void draw_contact_form(contact* buffer, bool viewing_only = false); +void draw_contact_form_ex(contact* buffer, bool viewing_only = false, bool with_autocomplete = false, bool* on_autocomplete = 0); void ui_setup_invoices() { @@ -17,23 +20,16 @@ void ui_setup_invoices() active_invoice = administration_create_empty_invoice(); } -bool draw_invoice_form(invoice* buffer, bool back_button_enabled = true, bool viewing_only = false) +void draw_invoice_form(invoice* buffer, bool viewing_only = false) { - if (back_button_enabled) - { - if (ImGui::Button(localize("form.back"))) { - current_view_state = view_state::LIST; - return false; - } - } - ImGui::Spacing(); - float widthAvailable = ImGui::GetContentRegionAvail().x; + //float widthAvailable = ImGui::GetContentRegionAvail().x; + ImGui::BeginDisabled(); // 1. Identifier - //ImGui::BeginDisabled(); //ImGui::SetNextItemWidth(widthAvailable*0.2f); //ImGui::InputText(localize("contact.form.identifier"), buffer->id, IM_ARRAYSIZE(buffer->id)); - + if (!viewing_only) ImGui::EndDisabled(); + // 2. Sequential number ImGui::Text("Invoice number: %s", buffer->sequential_number); @@ -42,20 +38,54 @@ bool draw_invoice_form(invoice* buffer, bool back_button_enabled = true, bool vi ImGui::Separator(); + // 4. Customer information ImGui::Text("Customer information"); - draw_contact_form(&buffer->customer); - strops_copy(buffer->customer_id, buffer->customer.id, sizeof(buffer->customer_id)); + bool on_autocomplete; + draw_contact_form_ex(&buffer->customer, false, true, &on_autocomplete); + + if (on_autocomplete) { + strops_copy(buffer->customer_id, buffer->customer.id, sizeof(buffer->customer_id)); + } ImGui::Separator(); + // 5. Invoice issued at + ImGui::BeginDisabled(); + 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("Invoice issued at"); + ImGui::EndDisabled(); + + // 6. Invoice expires at + ImGui::BeginDisabled(); + 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("Invoice expires at"); + ImGui::EndDisabled(); + + // 7. Product/service delivered at + 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("Product/service delivered at"); + //ImGui::SetNextItemWidth(widthAvailable*0.5f); //ImGui::InputTextWithHint("Invoice number", "Invoice number", buffer->sequential_number, IM_ARRAYSIZE(buffer->sequential_number)); //ImGui::SameLine();ui_helper_draw_required_tag(); - //if (!viewing_only) ImGui::EndDisabled(); - - return false; + if (viewing_only) ImGui::EndDisabled(); } void draw_invoices_list() @@ -75,12 +105,17 @@ void ui_draw_invoices() switch(current_view_state) { case view_state::LIST: draw_invoices_list(); break; - case view_state::CREATE: - if (draw_invoice_form(&active_invoice)) - { - //administration_create_invoice(active_invoice); + case view_state::CREATE: + + if (ImGui::Button(localize("form.back"))) { current_view_state = view_state::LIST; - } + } + draw_invoice_form(&active_invoice); + + //if () { + //administration_create_invoice(active_invoice); + //current_view_state = view_state::LIST; + //} break; case view_state::EDIT: break; case view_state::VIEW: break; diff --git a/src/ui/ui_projects.cpp b/src/ui/ui_projects.cpp index 5cb412f..3c550a2 100644 --- a/src/ui/ui_projects.cpp +++ b/src/ui/ui_projects.cpp @@ -45,7 +45,7 @@ static void draw_project_form() if (viewing_only) ImGui::EndDisabled(); if (!viewing_only) { - bool can_save = strlen(active_project.description) > 0; + bool can_save = administration_is_project_valid(active_project); if (!can_save) ImGui::BeginDisabled(); // Save button -- cgit v1.2.3-70-g09d2