kaui 4.0.14 → 4.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ <% modal_class ||= "" %>
2
+ <% search_fields ||= [] %>
3
+ <% date_fields ||= [] %>
4
+ <% number_fields ||= [] %>
5
+ <% search_query ||= "" %>
6
+ <% balance_notice ||= false %>
7
+
8
+ <div class="modal fade kaui-advanced-search-modal <%= modal_class %>" id="advanceSearchModal" tabindex="-1" role="dialog" aria-labelledby="advanceSearchModalLabel" aria-hidden="true"
9
+ data-save-search-label="<%= t('advanced_search.actions.save_search_as') %>"
10
+ data-save-label="<%= t('advanced_search.actions.save') %>"
11
+ data-empty-value-message="<%= t('advanced_search.validation.empty_value') %>"
12
+ data-missing-field-message="<%= t('advanced_search.validation.missing_field') %>">
13
+ <div class="modal-dialog" role="document">
14
+ <div class="modal-content">
15
+ <div class="modal-header">
16
+ <h5 class="modal-title d-flex align-items-center gap-3" id="advanceSearchModalLabel">
17
+ <span class="icon-container">
18
+ <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
19
+ </span>
20
+ <%= t('advanced_search.title') %>
21
+ </h5>
22
+ <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="<%= t('advanced_search.actions.close') %>">
23
+ <span aria-hidden="true">
24
+ <%= image_tag("kaui/modal/close.svg", width: 20, height: 20) %>
25
+ </span>
26
+ </button>
27
+ </div>
28
+ <div class="modal-body">
29
+ <form id="advanceSearchForm">
30
+ <div class="advanced-search-builder">
31
+ <div class="advanced-search-group">
32
+ <div id="search-fields-container" class="advanced-search-rules">
33
+ </div>
34
+ </div>
35
+ <button type="button" class="advanced-search-add-rule custom-hover" id="addSearchField">
36
+ <%= image_tag("kaui/modal/plus.svg", width: 16, height: 16) %>
37
+ <span><%= t('advanced_search.actions.add_search') %></span>
38
+ </button>
39
+ </div>
40
+ <div id="save-search-container" style="display: none;">
41
+ <hr class="mt-4 mb-3">
42
+ <div class="form-group d-flex align-items-center mb-0">
43
+ <label for="savedSearchName" class="mr-2 field-label" style="width: 30%;"><%= t('advanced_search.save_as_name') %></label>
44
+ <input type="text" id="savedSearchName" class="form-control flex-grow-1" placeholder="<%= t('advanced_search.save_name_placeholder') %>">
45
+ </div>
46
+ </div>
47
+ </form>
48
+ <% if balance_notice %>
49
+ <div class="alert alert-info advanced-search-alert" role="alert" style="margin-top: 30px;">
50
+ <%= t('advanced_search.balance_notice') %>
51
+ </div>
52
+ <% end %>
53
+ </div>
54
+ <div class="modal-footer">
55
+ <div class="advanced-search-footer-left">
56
+ <%= render "kaui/components/button/button", {
57
+ label: t('advanced_search.actions.save_search_as'),
58
+ variant: "advanced-search-button advanced-search-button-secondary d-inline-flex align-items-center gap-1",
59
+ type: "button",
60
+ html_class: "custom-hover",
61
+ html_options: {
62
+ id: "saveAdvanceSearch"
63
+ }
64
+ } %>
65
+ </div>
66
+ <div class="advanced-search-footer-right">
67
+ <%= render "kaui/components/button/button", {
68
+ label: t('advanced_search.actions.clear_search'),
69
+ variant: "advanced-search-button advanced-search-button-ghost d-inline-flex align-items-center gap-1",
70
+ type: "button",
71
+ html_class: "custom-hover",
72
+ html_options: {
73
+ id: "clearAdvanceSearch"
74
+ }
75
+ } %>
76
+ <%= render "kaui/components/button/button", {
77
+ label: t('advanced_search.actions.apply_search'),
78
+ variant: "advanced-search-button advanced-search-button-primary d-inline-flex align-items-center gap-1",
79
+ type: "button",
80
+ html_class: "custom-hover",
81
+ html_options: {
82
+ id: "applyAdvanceSearch"
83
+ }
84
+ } %>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <template id="search-field-template">
92
+ <div class="search-field">
93
+ <div class="advanced-search-rule-row">
94
+ <select class="form-control search-field-name" aria-label="<%= t('advanced_search.search_field') %>">
95
+ <option value="" selected><%= t('advanced_search.select_placeholder') %></option>
96
+ <% search_fields.each do |value, title| %>
97
+ <option value="<%= value %>"><%= title %></option>
98
+ <% end %>
99
+ </select>
100
+ <select class="form-control search-field-filter" aria-label="<%= t('advanced_search.compare_operation') %>">
101
+ <option value="eq"><%= t('advanced_search.operators.eq') %></option>
102
+ <option value="neq"><%= t('advanced_search.operators.neq') %></option>
103
+ <option value="gt"><%= t('advanced_search.operators.gt') %></option>
104
+ <option value="gte"><%= t('advanced_search.operators.gte') %></option>
105
+ <option value="lt"><%= t('advanced_search.operators.lt') %></option>
106
+ <option value="lte"><%= t('advanced_search.operators.lte') %></option>
107
+ <option value="like"><%= t('advanced_search.operators.like') %></option>
108
+ </select>
109
+ <input type="text" class="form-control search-field-value" aria-label="<%= t('advanced_search.search_value') %>">
110
+ <button type="button" class="button custom-hover remove-search-field" aria-label="<%= t('advanced_search.actions.remove_search') %>">
111
+ <%= image_tag("kaui/modal/red-close.svg", width: 16, height: 16) %>
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </template>
116
+
117
+ <%= javascript_tag do %>
118
+ $(document).ready(function() {
119
+
120
+ populateSearchLabelsFromUrl();
121
+ var dateFields = <%= raw date_fields.to_json %>;
122
+ var numberFields = <%= raw number_fields.to_json %>;
123
+ var emptyValueMessage = $('#advanceSearchModal').data('empty-value-message');
124
+ var missingFieldMessage = $('#advanceSearchModal').data('missing-field-message');
125
+
126
+ function updateSearchFieldRow(searchField) {
127
+ var selectedField = searchField.find('.search-field-name option:selected').text().trim();
128
+ var selectedValue = searchField.find('.search-field-name').val();
129
+ var valueInput = searchField.find('.search-field-value');
130
+
131
+ if (!selectedValue) {
132
+ searchField.find('.search-field-filter').attr('name', '');
133
+ valueInput.attr('name', '').attr('type', 'text');
134
+ return;
135
+ }
136
+
137
+ searchField.find('.search-field-filter').attr('name', selectedField + 'Filter');
138
+ valueInput.attr('name', selectedField);
139
+
140
+ if (dateFields.includes(selectedField)) {
141
+ valueInput.attr('type', 'date');
142
+ } else if (numberFields.includes(selectedField)) {
143
+ valueInput.attr('type', 'number');
144
+ } else {
145
+ valueInput.attr('type', 'text');
146
+ }
147
+ }
148
+
149
+ function addSearchField() {
150
+ var template = document.getElementById('search-field-template').content.cloneNode(true);
151
+ document.getElementById('search-fields-container').appendChild(template);
152
+ updateSearchFieldRow($('#search-fields-container .search-field:last'));
153
+ }
154
+
155
+ $('#addSearchField').on('click', function() {
156
+ addSearchField();
157
+ });
158
+
159
+ $('#search-fields-container').on('change', '.search-field-name', function() {
160
+ updateSearchFieldRow($(this).closest('.search-field'));
161
+ });
162
+
163
+ $('#applyAdvanceSearch').on('click', function() {
164
+ var searchFields = $('.search-field');
165
+ var searchLabelsContainer = $('#search-labels-container');
166
+
167
+ var hasValue = false;
168
+ var hasMissingField = false;
169
+ searchFields.each(function() {
170
+ var value = $(this).find('.search-field-value').val().trim();
171
+ if (value !== '') {
172
+ hasValue = true;
173
+ if (!$(this).find('.search-field-name').val()) {
174
+ hasMissingField = true;
175
+ }
176
+ }
177
+ });
178
+
179
+ if (!hasValue) {
180
+ alert(emptyValueMessage);
181
+ return;
182
+ }
183
+
184
+ if (hasMissingField) {
185
+ alert(missingFieldMessage);
186
+ return;
187
+ }
188
+
189
+ searchLabelsContainer.empty();
190
+
191
+ var table = $('<%= table_selector %>').DataTable();
192
+ table.off('preXhr.dt.filter');
193
+ table.on('preXhr.dt.filter', function(e, settings, data) {
194
+ data.search.value = searchQuery('<%= j search_query.to_s %>');
195
+ });
196
+
197
+ var searchParams = searchQuery('<%= j search_query.to_s %>');
198
+ var ajaxUrl = "<%= pagination_path %>";
199
+ if (searchParams) {
200
+ ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + searchParams;
201
+ }
202
+ table.ajax.url(ajaxUrl).load();
203
+
204
+ if (searchParams) {
205
+ searchParams = searchParams.replace(/account_id/g, 'ac_id');
206
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + searchParams;
207
+ window.history.pushState({ path: newUrl }, '', newUrl);
208
+ }
209
+
210
+ searchFields.each(function() {
211
+ var filter = $(this).find('.search-field-filter option:selected').text().trim();
212
+ var value = $(this).find('.search-field-value').val().trim();
213
+ var columnName = $(this).find('.search-field-name option:selected').text().trim();
214
+
215
+ if (value !== '') {
216
+ var label = $('<span>', {
217
+ class: 'label label-info d-inline-flex align-items-center gap-2',
218
+ 'data-field': columnName,
219
+ 'data-filter': filter,
220
+ 'data-value': value
221
+ });
222
+ var labelText = $('<span>', { text: columnName + ' [' + filter + '] ' + value });
223
+ var closeIcon = $('<span>', {
224
+ class: 'filter-close-icon',
225
+ style: 'cursor: pointer; margin-left: 5px; display: inline-flex; align-items: center;'
226
+ }).html('<svg width="12" height="12" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.8337 4.1665L4.16699 15.8332M4.16699 4.1665L15.8337 15.8332" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>');
227
+ label.append(labelText).append(closeIcon);
228
+ searchLabelsContainer.append(label);
229
+ }
230
+ });
231
+
232
+ $('#advanceSearchModal').modal('hide');
233
+ });
234
+
235
+ $('#advanceSearchModal').on('show.bs.modal', function() {
236
+ showAdvanceSearchModal();
237
+ if ($('#search-fields-container .search-field').length === 0) {
238
+ addSearchField();
239
+ }
240
+ $('#search-fields-container .search-field').each(function() {
241
+ updateSearchFieldRow($(this));
242
+ });
243
+ });
244
+
245
+ $('#clearAdvanceSearch').on('click', function() {
246
+ clearAdvanceSearch();
247
+ });
248
+
249
+ $('#search-fields-container').on('click', '.remove-search-field', function() {
250
+ $(this).closest('.search-field').remove();
251
+ });
252
+ });
253
+ <% end %>
@@ -44,6 +44,36 @@ en:
44
44
  loading_data_from_server: "Loading data from server"
45
45
  requested_amount_colon: "Requested amount: "
46
46
 
47
+ advanced_search:
48
+ title: "Advanced Search"
49
+ search_field: "Search field"
50
+ search_value: "Search value"
51
+ value_placeholder: "Enter search value..."
52
+ compare_operation: "Compare operation"
53
+ save_as_name: "Save As Name"
54
+ save_name_placeholder: "Enter a name for this search..."
55
+ select_placeholder: "-- Select --"
56
+ balance_notice: "* The Balance option won't work alongside other filters. If you search using Balance, all other filters will be ignored."
57
+ actions:
58
+ add_search: "Add Search"
59
+ apply_search: "Apply Search"
60
+ clear_search: "Clear Search"
61
+ close: "Close"
62
+ remove_search: "Remove search"
63
+ save: "Save"
64
+ save_search_as: "Save Search As..."
65
+ operators:
66
+ eq: "Equals"
67
+ neq: "Not Equals"
68
+ gt: "Greater Than"
69
+ gte: "Greater Than Or Equal"
70
+ lt: "Less Than"
71
+ lte: "Less Than Or Equal"
72
+ like: "Like"
73
+ validation:
74
+ empty_value: "Please enter a value for at least one search field."
75
+ missing_field: "Please select a search field for each rule with a value."
76
+
47
77
  flashes:
48
78
  notices:
49
79
  clock_updated_successfully: "Clock was successfully updated to %{new_date}"
@@ -51,6 +81,7 @@ en:
51
81
  overdue_uploaded_successfully: 'Overdue config was successfully uploaded'
52
82
  overdue_added_successfully: 'Overdue config was successfully added'
53
83
  overdue_updated_successfully: 'Overdue config was successfully updated'
84
+ overdue_deleted_successfully: 'Overdue config was successfully deleted'
54
85
  invoice_template_uploaded_successfully: 'Invoice template was successfully uploaded'
55
86
  invoice_translation_uploaded_successfully: 'Invoice translation was successfully uploaded'
56
87
  catalog_translation_uploaded_successfully: 'Catalog translation was successfully uploaded'
@@ -85,3 +116,4 @@ en:
85
116
 
86
117
  admin_tenants:
87
118
  clock_warning: "This action will affect all tenants across the system. Proceed with caution."
119
+ delete_overdue_config_confirmation: "This action is irreversible and will permanently delete the overdue configuration for this tenant. Are you sure you want to continue?"
data/config/routes.rb CHANGED
@@ -25,7 +25,7 @@ Kaui::Engine.routes.draw do
25
25
  get '/500', to: 'errors#show', code: 500
26
26
 
27
27
  scope '/accounts' do
28
- get '/pagination' => 'accounts#pagination', :as => 'accounts_pagination'
28
+ post '/pagination' => 'accounts#pagination', :as => 'accounts_pagination'
29
29
  get '/validate_external_key' => 'accounts#validate_external_key', :as => 'accounts_validate_external_key'
30
30
  get '/download' => 'accounts#download', :as => 'download_accounts'
31
31
  get '/export/:account_id', to: 'accounts#export_account', as: 'export_account'
@@ -189,6 +189,7 @@ Kaui::Engine.routes.draw do
189
189
  delete '/:id/delete_catalog' => 'admin_tenants#delete_catalog', :as => 'admin_tenant_delete_catalog'
190
190
  get '/:id/new_plan_currency' => 'admin_tenants#new_plan_currency', :as => 'admin_tenant_new_plan_currency'
191
191
  get '/:id/new_overdue_config' => 'admin_tenants#new_overdue_config', :as => 'admin_tenant_new_overdue_config'
192
+ delete '/:id' => 'admin_tenants#delete_overdue_config', :as => 'admin_tenant_delete_overdue_config'
192
193
  post '/upload_catalog' => 'admin_tenants#upload_catalog', :as => 'admin_tenant_upload_catalog'
193
194
  post '/display_catalog_xml' => 'admin_tenants#display_catalog_xml', :as => 'admin_tenant_display_catalog_xml'
194
195
  post '/display_overdue_xml' => 'admin_tenants#display_overdue_xml', :as => 'admin_tenant_display_overdue_xml'
@@ -26,7 +26,8 @@ module Kaui
26
26
  end
27
27
 
28
28
  def perform_redirect_after_error(error:, error_message:, redirect: true)
29
- account_id = nested_hash_value(params.permit!.to_h, :account_id)
29
+ account_id = request.path_parameters[:account_id].presence || nested_hash_value(params.permit!.to_h, :account_id)
30
+ account_id = nil unless account_id.is_a?(String) || account_id.is_a?(Numeric)
30
31
  home_path = kaui_engine.home_path
31
32
  redirect_path = if redirect && account_id.present?
32
33
  kaui_engine.account_path(account_id)
data/lib/kaui/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kaui
4
- VERSION = '4.0.14'
4
+ VERSION = '4.0.16'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaui
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.14
4
+ version: 4.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kill Bill core team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-28 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -644,6 +644,7 @@ files:
644
644
  - app/views/kaui/role_definitions/new.html.erb
645
645
  - app/views/kaui/sessions/_form.html.erb
646
646
  - app/views/kaui/sessions/new.html.erb
647
+ - app/views/kaui/shared/_advanced_search_filterbar.html.erb
647
648
  - app/views/kaui/subscriptions/_cancel_by_date_modal.html.erb
648
649
  - app/views/kaui/subscriptions/_edit_form.html.erb
649
650
  - app/views/kaui/subscriptions/_form.html.erb