kaui 4.0.13 → 4.0.15

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/kaui/save.svg +5 -0
  3. data/app/assets/javascripts/kaui/multi_functions_bar_utils.js +183 -7
  4. data/app/assets/stylesheets/kaui/common.css +4 -1
  5. data/app/assets/stylesheets/kaui/subscription.css +1 -1
  6. data/app/controllers/kaui/admin_tenants_controller.rb +37 -7
  7. data/app/controllers/kaui/engine_controller.rb +2 -1
  8. data/app/controllers/kaui/engine_controller_util.rb +26 -2
  9. data/app/controllers/kaui/invoices_controller.rb +3 -3
  10. data/app/controllers/kaui/payments_controller.rb +3 -3
  11. data/app/views/kaui/accounts/_account_filterbar.html.erb +34 -1
  12. data/app/views/kaui/accounts/_form_account.html.erb +1 -1
  13. data/app/views/kaui/accounts/_functions_bar.html.erb +3 -1
  14. data/app/views/kaui/accounts/edit.html.erb +9 -0
  15. data/app/views/kaui/accounts/index.html.erb +20 -0
  16. data/app/views/kaui/accounts/new.html.erb +14 -0
  17. data/app/views/kaui/admin_tenants/_show_overdue.erb +1 -1
  18. data/app/views/kaui/admin_tenants/new_overdue_config.html.erb +13 -0
  19. data/app/views/kaui/bundles/_bundle_filterbar.html.erb +15 -1
  20. data/app/views/kaui/bundles/index.html.erb +8 -1
  21. data/app/views/kaui/components/menu_dropdown/_menu_dropdown.html.erb +2 -2
  22. data/app/views/kaui/invoices/_invoice_filterbar.html.erb +42 -4
  23. data/app/views/kaui/invoices/_multi_functions_bar.html.erb +9 -10
  24. data/app/views/kaui/invoices/index.html.erb +15 -3
  25. data/app/views/kaui/layouts/kaui_application.html.erb +10 -10
  26. data/app/views/kaui/layouts/kaui_flash.html.erb +5 -26
  27. data/app/views/kaui/payments/_multi_functions_bar.html.erb +9 -9
  28. data/app/views/kaui/payments/_payment_filterbar.html.erb +42 -4
  29. data/app/views/kaui/payments/index.html.erb +12 -0
  30. data/config/locales/en.yml +3 -0
  31. data/config/routes.rb +1 -0
  32. data/lib/kaui/error_handler.rb +2 -1
  33. data/lib/kaui/version.rb +1 -1
  34. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62d467e5b936f1738863675c8f5747e9ea4187ac28670556e09d857b1921d588
4
- data.tar.gz: c79e0a54c96cb563b1eaf7129f147240fd8da04e784a284f7390c39a7b5ac587
3
+ metadata.gz: 3452f98093718cc5761ee58798ac78467b66d046fc5cf17556060bd02f7637ce
4
+ data.tar.gz: 555af1e3377e9fcb143716618644e2701138d2a731c7f970761691a0d3ecd0fa
5
5
  SHA512:
6
- metadata.gz: f6ff3a1e16012a78cfed10bfaf51ce9733f22d9ebf624d539cdf03cf3a8d7f8dcbda547022dc9258334ae9411c32a0261a7a81b40b19000218a0142950297198
7
- data.tar.gz: fa4d96b4ccb3afea63726fa9d6a2c2e0f5c853ce8ce5e5acf518c839ab0487fd9c5952ccf33d0b21307b3703ac48b917c15e486061628fb8462091280fb8d21c
6
+ metadata.gz: 30d0269df326b5db8336c910ee4a111da1101d5212c43b728fbac297015b9a18409ffed04978867194bac6e7feda6beea4ea101469e201ab6465de74c863da6d
7
+ data.tar.gz: 3c289f1741f5ddda2c614adfca575dff1b540ea56cde455beb8fb870bbb4c898c9c793ffd0b991818fe6883f55c50a4412efbd45b0c9d2f0180ac67c7637698c
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
3
+ <polyline points="17 21 17 13 7 13 7 21"/>
4
+ <polyline points="7 3 7 8 15 8"/>
5
+ </svg>
@@ -84,11 +84,6 @@ function populateSearchLabelsFromUrl() {
84
84
 
85
85
  function searchQuery(account_id){
86
86
  var searchFields = $('.search-field');
87
- var searchLabelsContainer = $('#search-labels-container');
88
-
89
- if (searchFields.length > 0) {
90
- searchLabelsContainer.empty();
91
- }
92
87
  var searchLabels = '';
93
88
  if (searchFields.length > 0) {
94
89
  searchLabels = searchFields.map(function() {
@@ -135,17 +130,25 @@ function searchQuery(account_id){
135
130
  };
136
131
 
137
132
  function clearAdvanceSearch() {
133
+ var hasActiveSearch = $('#search-labels-container .label').length > 0 ||
134
+ window.location.search.includes('_q=1');
135
+
138
136
  // Clear all search fields
139
137
  $('#search-fields-container').empty();
140
138
 
141
139
  // Remove all search labels
142
140
  $('#search-labels-container').empty();
143
141
 
144
- // Reload the page with the original URL (no parameters)
145
- window.location.href = window.location.pathname;
142
+ // Clear Persisted Search for this page
143
+ localStorage.removeItem('kaui_adv_search_' + window.location.pathname);
146
144
 
147
145
  // Hide the modal
148
146
  $('#advanceSearchModal').modal('hide');
147
+
148
+ // Only reload if there was an active search to clear
149
+ if (hasActiveSearch) {
150
+ window.location.href = window.location.pathname;
151
+ }
149
152
  }
150
153
 
151
154
  function showAdvanceSearchModal() {
@@ -230,4 +233,177 @@ $(document).on('click', '.filter-close-icon', function() {
230
233
  var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
231
234
  window.history.pushState({ path: newUrl }, '', newUrl);
232
235
  }
236
+ });
237
+
238
+ // --- Saved Searches Logic ---
239
+ function getSavedSearches() {
240
+ var key = 'kaui_saved_searches_' + window.location.pathname;
241
+ var data = localStorage.getItem(key);
242
+ if (!data) return {};
243
+ try {
244
+ var parsed = JSON.parse(data);
245
+ if (typeof parsed === 'object' && parsed !== null) {
246
+ return parsed;
247
+ }
248
+ } catch (e) {
249
+ // Legacy string format or invalid JSON
250
+ var legacy = {};
251
+ legacy["Default"] = data;
252
+ localStorage.setItem(key, JSON.stringify(legacy));
253
+ return legacy;
254
+ }
255
+ return {};
256
+ }
257
+
258
+ function saveSavedSearches(searches) {
259
+ var key = 'kaui_saved_searches_' + window.location.pathname;
260
+ localStorage.setItem(key, JSON.stringify(searches));
261
+ }
262
+
263
+ function populateSavedSearchesDropdown() {
264
+ var menu = $('#savedSearchesDropdown_menu');
265
+ if (!menu.length) return;
266
+
267
+ menu.empty();
268
+ var searches = getSavedSearches();
269
+ var names = Object.keys(searches);
270
+
271
+ var dropdownButton = $('#savedSearchesDropdown_button');
272
+ if (names.length === 0) {
273
+ menu.append('<li><span class="dropdown-item text-muted">No saved searches</span></li>');
274
+ return;
275
+ }
276
+
277
+ names.forEach(function(name) {
278
+ var li = $('<li>');
279
+ var a = $('<a>', {
280
+ class: 'dropdown-item d-flex align-items-center justify-content-between apply-saved-search',
281
+ href: 'javascript:void(0);',
282
+ 'data-name': name,
283
+ style: 'cursor: pointer;'
284
+ });
285
+
286
+ var nameSpan = $('<span>', {
287
+ class: 'text-black-700 fs-6 text-normal',
288
+ style: 'font-weight: 500; font-size: 0.875rem !important; line-height: 1.25rem;',
289
+ text: name
290
+ });
291
+
292
+ var deleteIcon = $('<span>', {
293
+ class: 'delete-saved-search text-danger',
294
+ 'data-name': name,
295
+ style: 'cursor: pointer; padding: 0 5px; font-weight: bold;',
296
+ html: '&times;',
297
+ title: 'Delete saved search'
298
+ });
299
+
300
+ a.append(nameSpan).append(deleteIcon);
301
+ li.append(a);
302
+ menu.append(li);
303
+ });
304
+ }
305
+
306
+ $(document).ready(function() {
307
+ populateSavedSearchesDropdown();
308
+
309
+ $(document).on('submit', '#advanceSearchForm', function(e) {
310
+ e.preventDefault();
311
+ return false;
312
+ });
313
+
314
+ $(document).on('input', '#savedSearchName', function() {
315
+ var val = $(this).val().trim();
316
+ if (val === '') {
317
+ $('#saveAdvanceSearch').text('Save Search As...');
318
+ } else {
319
+ $('#saveAdvanceSearch').text('Save');
320
+ }
321
+ });
322
+
323
+ $(document).on('click', '#saveAdvanceSearch', function(e) {
324
+ e.preventDefault();
325
+ var container = $('#save-search-container');
326
+ var input = $('#savedSearchName');
327
+
328
+ if (!container.is(':visible')) {
329
+ container.show();
330
+ input.focus();
331
+ return;
332
+ }
333
+
334
+ var name = input.val().trim();
335
+ if (name === '') {
336
+ container.hide();
337
+ return;
338
+ }
339
+
340
+ var searchParams = '';
341
+
342
+ // For bundles, it's search_by and q. For everything else, it's searchQuery()
343
+ if (window.location.pathname.includes('/bundles')) {
344
+ var searchBy = $('#searchFieldSelect').val();
345
+ var q = $('#bundleSearchValue').val();
346
+ if (q) q = q.trim();
347
+ if (!q) {
348
+ alert("Please enter a value to search.");
349
+ return;
350
+ }
351
+ searchParams = 'search_by=' + encodeURIComponent(searchBy) + '&q=' + encodeURIComponent(q);
352
+ } else {
353
+ // Check if there are any search fields before calling searchQuery(),
354
+ // because searchQuery() calls clearAdvanceSearch() (which reloads the page)
355
+ // when no fields exist.
356
+ if ($('.search-field').length === 0 && $('#search-labels-container .label').length === 0) {
357
+ alert("Please enter at least one search field.");
358
+ return;
359
+ }
360
+ searchParams = searchQuery();
361
+ if (!searchParams) {
362
+ alert("Please enter at least one search field.");
363
+ return;
364
+ }
365
+ searchParams = searchParams.replace(/account_id/g, 'ac_id');
366
+ }
367
+
368
+ var searches = getSavedSearches();
369
+ searches[name] = searchParams;
370
+ saveSavedSearches(searches);
371
+
372
+ input.val('');
373
+ $('#saveAdvanceSearch').text('Save Search As...');
374
+ container.hide();
375
+
376
+ populateSavedSearchesDropdown();
377
+ $('#applyAdvanceSearch').trigger('click');
378
+ });
379
+
380
+ $(document).on('hidden.bs.modal', '#advanceSearchModal', function () {
381
+ $('#save-search-container').hide();
382
+ $('#savedSearchName').val('');
383
+ $('#saveAdvanceSearch').text('Save Search As...');
384
+ });
385
+
386
+ $(document).on('click', '.apply-saved-search', function(e) {
387
+ // If they clicked the delete icon, don't apply the search
388
+ if ($(e.target).hasClass('delete-saved-search')) return;
389
+
390
+ var name = $(this).data('name');
391
+ var searches = getSavedSearches();
392
+ var params = searches[name];
393
+ if (params) {
394
+ var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + params;
395
+ window.location.href = newUrl;
396
+ }
397
+ });
398
+
399
+ $(document).on('click', '.delete-saved-search', function(e) {
400
+ e.stopPropagation(); // prevent dropdown from closing and prevent applying search
401
+ var name = $(this).data('name');
402
+ if (confirm("Are you sure you want to delete the saved search '" + name + "'?")) {
403
+ var searches = getSavedSearches();
404
+ delete searches[name];
405
+ saveSavedSearches(searches);
406
+ populateSavedSearchesDropdown();
407
+ }
408
+ });
233
409
  });
@@ -1293,7 +1293,6 @@ table thead th:last-child {
1293
1293
  align-items: center;
1294
1294
  gap: 0.625rem;
1295
1295
  padding: 0.875rem 1.125rem;
1296
- margin: 0.625rem 0;
1297
1296
  border-radius: 0.375rem;
1298
1297
  font-size: 0.9375rem;
1299
1298
  font-weight: 500;
@@ -1467,6 +1466,10 @@ table thead th:last-child {
1467
1466
  width: 100%;
1468
1467
  }
1469
1468
 
1469
+ .field_with_errors {
1470
+ display: contents;
1471
+ }
1472
+
1470
1473
  /* Navbar color changes - Higher specificity to override existing rules */
1471
1474
  header .header-icon {
1472
1475
  color: #374151 !important;
@@ -691,7 +691,7 @@ table tr.expired td {
691
691
  margin-bottom: 0.25rem;
692
692
  }
693
693
 
694
- /* Advance Search modal – Subscription Bundles page */
694
+ /* Advanced Search modal – Subscription Bundles page */
695
695
  .subscription-bundl-index .close-button {
696
696
  background: transparent;
697
697
  padding: 0;
@@ -61,8 +61,8 @@ module Kaui
61
61
  nil
62
62
  end
63
63
 
64
- @overdue = wait(fetch_overdue)
65
- @overdue_xml = wait(fetch_overdue_xml)
64
+ @overdue = flash[:overdue_deleted] ? nil : wait(fetch_overdue)
65
+ @overdue_xml = flash[:overdue_deleted] ? nil : wait(fetch_overdue_xml)
66
66
  @tenant_plugin_config = begin
67
67
  wait(fetch_tenant_plugin_config)
68
68
  rescue StandardError
@@ -288,13 +288,19 @@ module Kaui
288
288
  options = tenant_options_for_client
289
289
  options[:api_key] = @tenant.api_key
290
290
  options[:api_secret] = @tenant.api_secret
291
+ @overdue_config_exists = false
291
292
  begin
292
293
  @overdue = Kaui::Overdue.get_overdue_json(options)
293
294
  @overdue_corrupted = false
295
+ @overdue_config_exists = @overdue.overdue_states.present? && !@overdue.has_states
296
+ rescue KillBillClient::API::NotFound
297
+ @overdue = KillBillClient::Model::Overdue.new.tap { |o| o.overdue_states = [] }
298
+ @overdue_corrupted = false
294
299
  rescue StandardError => e
295
300
  Rails.logger.warn("Failed to load overdue configuration for tenant #{@tenant.id}: #{e.class}: #{e.message}")
296
301
  @overdue = KillBillClient::Model::Overdue.new.tap { |o| o.overdue_states = [] }
297
302
  @overdue_corrupted = true
303
+ @overdue_config_exists = true
298
304
  flash.now[:warning] = 'The existing overdue configuration is corrupted and cannot be loaded. Use the XML upload below to replace it with a valid configuration.'
299
305
  end
300
306
  end
@@ -306,12 +312,18 @@ module Kaui
306
312
  options[:api_key] = current_tenant.api_key
307
313
  options[:api_secret] = current_tenant.api_secret
308
314
 
309
- view_form_model = params.require(:kill_bill_client_model_overdue).permit!.to_h.compact_blank
315
+ overdue_params = params[:kill_bill_client_model_overdue]&.permit!
316
+ view_form_model = overdue_params&.to_h&.compact_blank || {}
310
317
  view_form_model['states'] = view_form_model['states'].values if view_form_model['states'].present?
311
-
312
- overdue = Kaui::Overdue.from_overdue_form_model(view_form_model)
313
- Kaui::Overdue.upload_tenant_overdue_config_json(overdue.to_json, options[:username], nil, comment, options)
314
- redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_added_successfully')
318
+ if view_form_model['states'].blank?
319
+ Kaui::AdminTenant.delete_tenant_user_key_value('OVERDUE_CONFIG', options[:username], nil, comment, options)
320
+ flash[:overdue_deleted] = true
321
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_updated_successfully')
322
+ else
323
+ overdue = Kaui::Overdue.from_overdue_form_model(view_form_model)
324
+ Kaui::Overdue.upload_tenant_overdue_config_json(overdue.to_json, options[:username], nil, comment, options)
325
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_added_successfully')
326
+ end
315
327
  end
316
328
 
317
329
  def upload_overdue_config
@@ -336,6 +348,24 @@ module Kaui
336
348
  redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_uploaded_successfully')
337
349
  end
338
350
 
351
+ def delete_overdue_config
352
+ current_tenant = safely_find_tenant_by_id(params[:id])
353
+
354
+ options = tenant_options_for_client
355
+ options[:api_key] = current_tenant.api_key
356
+ options[:api_secret] = current_tenant.api_secret
357
+
358
+ begin
359
+ Kaui::AdminTenant.delete_tenant_user_key_value('OVERDUE_CONFIG', options[:username], nil, comment, options)
360
+ rescue StandardError => e
361
+ flash[:error] = "Failed to delete overdue config: #{as_string(e)}"
362
+ redirect_to admin_tenant_new_overdue_config_path(id: current_tenant.id) and return
363
+ end
364
+
365
+ flash[:overdue_deleted] = true
366
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_deleted_successfully')
367
+ end
368
+
339
369
  def upload_invoice_template
340
370
  current_tenant = safely_find_tenant_by_id(params[:id])
341
371
 
@@ -38,7 +38,8 @@ class Kaui::EngineController < ApplicationController
38
38
  end
39
39
 
40
40
  def populate_account_details
41
- @account ||= params[:account_id].present? ? Kaui::Account.find_by_id(params[:account_id], false, false, options_for_klient) : Kaui::Account.new
41
+ account_id = scalar_account_id_param
42
+ @account ||= account_id.present? ? Kaui::Account.find_by_id(account_id, false, false, options_for_klient) : Kaui::Account.new
42
43
  end
43
44
 
44
45
  def retrieve_tenants_for_current_user
@@ -37,8 +37,8 @@ module Kaui
37
37
  end
38
38
 
39
39
  def paginate(searcher, data_extractor, formatter, table_default_columns = [])
40
- search_key = (params[:search] || {})[:value].presence
41
- advance_search_query = params[:advance_search_query].presence
40
+ search_key = normalize_search_key((params[:search] || {})[:value]).presence
41
+ advance_search_query = normalize_search_key(params[:advance_search_query]).presence
42
42
 
43
43
  search_key = advance_search_query if advance_search_query
44
44
  search_key = handle_balance_search(search_key) if search_key.present?
@@ -84,6 +84,30 @@ module Kaui
84
84
  end
85
85
  end
86
86
 
87
+ def advanced_search_query?(search_key)
88
+ search_key.to_s.include?('_q')
89
+ end
90
+
91
+ def scalar_account_id_param
92
+ path_account_id = request.path_parameters[:account_id].presence
93
+ return path_account_id if path_account_id.present?
94
+
95
+ account_id = params[:account_id]
96
+ account_id if account_id.is_a?(String) || account_id.is_a?(Numeric)
97
+ end
98
+
99
+ def normalize_search_key(search_key)
100
+ return if search_key.blank?
101
+
102
+ if search_key.respond_to?(:to_unsafe_h)
103
+ search_key.to_unsafe_h.to_query
104
+ elsif search_key.is_a?(Hash)
105
+ search_key.to_query
106
+ else
107
+ search_key
108
+ end
109
+ end
110
+
87
111
  def promise(&)
88
112
  # Evaluation starts immediately
89
113
  ::Concurrent::Promises.future do
@@ -4,7 +4,7 @@ require 'csv'
4
4
  module Kaui
5
5
  class InvoicesController < Kaui::EngineController
6
6
  def index
7
- @search_query = params[:account_id]
7
+ @search_query = scalar_account_id_param
8
8
  @advance_search_query = params[:q] || request.query_string
9
9
  @ordering = params[:ordering] || 'desc'
10
10
  @offset = params[:offset] || 0
@@ -15,7 +15,7 @@ module Kaui
15
15
  end
16
16
 
17
17
  def download
18
- account_id = params[:account_id]
18
+ account_id = scalar_account_id_param
19
19
  start_date = params[:startDate]
20
20
  end_date = params[:endDate]
21
21
  all_fields_checked = params[:allFieldsChecked] == 'true'
@@ -60,7 +60,7 @@ module Kaui
60
60
 
61
61
  searcher = lambda do |search_key, offset, limit|
62
62
  account = begin
63
- Kaui::Account.find_by_id_or_key(search_key, false, false, cached_options_for_klient)
63
+ Kaui::Account.find_by_id_or_key(search_key, false, false, cached_options_for_klient) unless advanced_search_query?(search_key)
64
64
  rescue StandardError
65
65
  nil
66
66
  end
@@ -5,7 +5,7 @@ require 'csv'
5
5
  module Kaui
6
6
  class PaymentsController < Kaui::EngineController
7
7
  def index
8
- @search_query = params[:q] || params[:account_id]
8
+ @search_query = params[:q] || scalar_account_id_param
9
9
  @advance_search_query = params[:q] || request.query_string
10
10
  @ordering = params[:ordering] || (@search_query.blank? ? 'desc' : 'asc')
11
11
  @offset = params[:offset] || 0
@@ -16,7 +16,7 @@ module Kaui
16
16
  end
17
17
 
18
18
  def download
19
- account_id = params[:account_id]
19
+ account_id = scalar_account_id_param
20
20
  start_date = params[:startDate]
21
21
  end_date = params[:endDate]
22
22
  all_fields_checked = params[:allFieldsChecked] == 'true'
@@ -104,7 +104,7 @@ module Kaui
104
104
  payments = Kaui::Payment.list_or_search(payment_state, offset, limit, options_for_klient)
105
105
  else
106
106
  account = begin
107
- Kaui::Account.find_by_id_or_key(search_key, false, false, options_for_klient)
107
+ Kaui::Account.find_by_id_or_key(search_key, false, false, options_for_klient) unless advanced_search_query?(search_key)
108
108
  rescue StandardError
109
109
  nil
110
110
  end
@@ -6,7 +6,7 @@
6
6
  <span class="icon-container">
7
7
  <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
8
8
  </span>
9
- Advance Search
9
+ Advanced Search
10
10
  </h5>
11
11
  <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
12
12
  <span aria-hidden="true">
@@ -29,9 +29,25 @@
29
29
  </div>
30
30
  <div id="search-fields-container">
31
31
  </div>
32
+ <div id="save-search-container" style="display: none;">
33
+ <hr class="mt-4 mb-3">
34
+ <div class="form-group d-flex align-items-center mb-0">
35
+ <label for="savedSearchName" class="mr-2 field-label" style="width: 30%;">Save As Name</label>
36
+ <input type="text" id="savedSearchName" class="form-control flex-grow-1" placeholder="Enter a name for this search...">
37
+ </div>
38
+ </div>
32
39
  </form>
33
40
  </div>
34
41
  <div class="modal-footer">
42
+ <%= render "kaui/components/button/button", {
43
+ label: 'Save Search As...',
44
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
45
+ type: "button",
46
+ html_class: "kaui-button custom-hover",
47
+ html_options: {
48
+ id: "saveAdvanceSearch"
49
+ }
50
+ } %>
35
51
  <%= render "kaui/components/button/button", {
36
52
  label: 'Clear Search',
37
53
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
@@ -80,6 +96,7 @@
80
96
 
81
97
  <%= javascript_tag do %>
82
98
  $(document).ready(function() {
99
+
83
100
  populateSearchLabelsFromUrl();
84
101
  var dateFields = ['Created date', 'Updated date', 'Reference time'];
85
102
  // Handle the "Add" button click to add new search fields
@@ -109,6 +126,22 @@ $(document).ready(function() {
109
126
  var searchLabelsContainer = $('#search-labels-container');
110
127
  searchLabelsContainer.empty();
111
128
 
129
+ // Validate that at least one search field has a value
130
+ var hasValue = false;
131
+ searchFields.each(function() {
132
+ var value = $(this).find('.search-field-value').val().trim();
133
+ if (value !== '') {
134
+ hasValue = true;
135
+ return false; // Break the loop
136
+ }
137
+ });
138
+
139
+ // If no search field has a value, show an alert and prevent search
140
+ if (!hasValue) {
141
+ alert('Please enter a value for at least one search field.');
142
+ return;
143
+ }
144
+
112
145
  var table = $('#accounts-table').DataTable();
113
146
  table.off('preXhr.dt.filter');
114
147
  table.on('preXhr.dt.filter', function(e, settings, data) {
@@ -117,7 +117,7 @@
117
117
  <div class="form-group d-flex pb-3">
118
118
  <%= f.label :phone, 'Phone', :class => 'col-sm-3 control-label' %>
119
119
  <div class="col-sm-9">
120
- <%= f.number_field :phone, :class => 'form-control' %>
120
+ <%= f.text_field :phone, :class => 'form-control' %>
121
121
  </div>
122
122
  </div>
123
123
  <div class="form-group d-flex pb-3 <%= 'border-bottom mb-3' if @account.persisted? %>">
@@ -8,7 +8,9 @@
8
8
  <%= render "kaui/components/menu_dropdown/menu_dropdown", {
9
9
  label: action[:label],
10
10
  icon: action[:icon],
11
- dropdown_id: "menuDropdown#{index}",
11
+ variant: action[:variant] ? action[:variant] : "btn btn-primary",
12
+ html_class: action[:html_class] || "",
13
+ dropdown_id: action[:id] || "menuDropdown#{index}",
12
14
  menu_items: action[:items]
13
15
  } %>
14
16
  <% else %>
@@ -1,6 +1,15 @@
1
1
  <div class="register kaui-new-account">
2
2
  <div class="">
3
3
  <div class="mx-auto" style="max-width: 37.5rem;">
4
+ <% if @account.errors.any? %>
5
+ <div class="custom-alert custom-alert-danger mb-3">
6
+ <ul class="mb-0">
7
+ <% @account.errors.each do |error| %>
8
+ <li><%= error.message %></li>
9
+ <% end %>
10
+ </ul>
11
+ </div>
12
+ <% end %>
4
13
  <h5 class="add-account-title">
5
14
  <span class="icon-container">
6
15
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -19,6 +19,14 @@
19
19
  bs_target: "#advanceSearchModal"
20
20
  }
21
21
  },
22
+ {
23
+ type: "dropdown",
24
+ label: "Saved Searches",
25
+ icon: 'kaui/save.svg',
26
+ variant: "btn btn-outline-secondary d-inline-flex align-items-center gap-1 kaui-button custom-hover",
27
+ id: "savedSearchesDropdown",
28
+ items: []
29
+ },
22
30
  {
23
31
  label: "Download CSV",
24
32
  icon: 'kaui/download.svg',
@@ -114,6 +122,18 @@ $(document).ready(function() {
114
122
  }
115
123
  });
116
124
 
125
+ // If the page loaded with advanced search params in the URL (e.g. restored from
126
+ // localStorage) but @advance_search_query was not set server-side, re-fire the
127
+ // DataTable request with the correct filters.
128
+ if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '') {
129
+ var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
130
+ var ajaxUrl = "<%= accounts_pagination_path(:ordering => @ordering, :format => :json) %>" + '?' + urlSearch;
131
+ table.on('preXhr.dt.filter', function(e, settings, data) {
132
+ data.search.value = urlSearch;
133
+ });
134
+ table.ajax.url(ajaxUrl).load();
135
+ }
136
+
117
137
  <!-- When we don't know the total number of pages, we need to hide the legend and next button manually -->
118
138
  $('#accounts-table').on('draw.dt', function() {
119
139
  <% if @max_nb_records.nil? %>
@@ -1,6 +1,20 @@
1
1
  <div class="register kaui-new-account">
2
2
  <div class="">
3
3
  <div class="mx-auto" style="max-width: 37.5rem;">
4
+ <% if @account.errors.any? %>
5
+ <script>
6
+ if (/\/accounts$/.test(window.location.pathname)) {
7
+ window.history.replaceState({}, '', window.location.pathname + '/new');
8
+ }
9
+ </script>
10
+ <div class="custom-alert custom-alert-danger mb-3">
11
+ <ul class="mb-0">
12
+ <% @account.errors.each do |error| %>
13
+ <li><%= error.message %></li>
14
+ <% end %>
15
+ </ul>
16
+ </div>
17
+ <% end %>
4
18
  <h5 class="add-account-title">
5
19
  <span class="icon-container">
6
20
  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -32,7 +32,7 @@
32
32
  <table id="existing-overdue-config-for-tenants" class="existing-overdue-config-for-tenants-table">
33
33
  <span class='label'>
34
34
  </span>
35
- <% if @overdue.nil? || @overdue.has_states %>
35
+ <% if @overdue.nil? || @overdue.overdue_states.empty? || @overdue.has_states %>
36
36
  No overdue configuration defined for tenant
37
37
  <% else %>
38
38
  <thead>
@@ -10,6 +10,19 @@
10
10
  </span>
11
11
  Overdue Configuration
12
12
  </h5>
13
+ <% if @overdue_config_exists && can?(:config_upload, Kaui::AdminTenant) %>
14
+ <% overdue_delete_confirmation = t('admin_tenants.delete_overdue_config_confirmation') %>
15
+ <div class="form-group d-flex justify-content-end pb-3">
16
+ <%= form_tag(kaui_engine.admin_tenant_delete_overdue_config_path(id: @tenant.id), method: :delete, class: 'd-inline', onsubmit: "return confirm('#{j(overdue_delete_confirmation)}');") do %>
17
+ <%= render "kaui/components/button/button", {
18
+ label: 'Delete',
19
+ variant: "d-inline-flex align-items-center gap-1",
20
+ type: "submit",
21
+ html_class: "kaui-button delete-button custom-hover"
22
+ } %>
23
+ <% end %>
24
+ </div>
25
+ <% end %>
13
26
  <div class="form-group d-flex pb-3">
14
27
  <label class="col-sm-1 control-label">Type</label>
15
28
  <div class="toggle-segment col-sm-9">
@@ -6,7 +6,7 @@
6
6
  <span class="icon-container">
7
7
  <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
8
8
  </span>
9
- Advance Search
9
+ Advanced Search
10
10
  </h5>
11
11
  <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
12
12
  <span aria-hidden="true">
@@ -28,9 +28,23 @@
28
28
  <label for="bundleSearchValue" class="mr-2 field-label" style="width: 30%;">Value</label>
29
29
  <input type="text" id="bundleSearchValue" class="form-control" placeholder="Enter search value..." value="<%= @search_query %>">
30
30
  </div>
31
+ <div id="save-search-container" style="display: none;">
32
+ <hr class="mt-4 mb-3">
33
+ <div class="form-group d-flex align-items-center mb-0">
34
+ <label for="savedSearchName" class="mr-2 field-label" style="width: 30%;">Save As Name</label>
35
+ <input type="text" id="savedSearchName" class="form-control flex-grow-1" placeholder="Enter a name for this search...">
36
+ </div>
37
+ </div>
31
38
  </form>
32
39
  </div>
33
40
  <div class="modal-footer">
41
+ <%= render "kaui/components/button/button", {
42
+ label: 'Save Search As...',
43
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
44
+ type: "button",
45
+ html_class: "kaui-button custom-hover",
46
+ html_options: { id: "saveAdvanceSearch" }
47
+ } %>
34
48
  <%= render "kaui/components/button/button", {
35
49
  label: 'Clear Search',
36
50
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
@@ -11,7 +11,7 @@
11
11
  </div>
12
12
  <span class="d-flex align-items-center gap-2">
13
13
  <%= render "kaui/components/button/button", {
14
- label: "Advance Search",
14
+ label: "Advanced Search",
15
15
  icon: "kaui/search.svg",
16
16
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
17
17
  type: "button",
@@ -23,6 +23,13 @@
23
23
  }
24
24
  }
25
25
  } %>
26
+ <%= render "kaui/components/menu_dropdown/menu_dropdown", {
27
+ label: "Saved Searches",
28
+ icon: "kaui/save.svg",
29
+ variant: "btn btn-outline-secondary d-inline-flex align-items-center gap-1 kaui-button custom-hover",
30
+ dropdown_id: "savedSearchesDropdown",
31
+ menu_items: []
32
+ } %>
26
33
  <% if can? :create, Kaui::Subscription %>
27
34
  <%= link_to kaui_engine.new_subscription_path(:params => { :account_id => @account.account_id, :product_category => 'BASE' }) do %>
28
35
  <%= render "kaui/components/button/button", {
@@ -1,4 +1,4 @@
1
- <% variant ||= "btn-primary" %>
1
+ <% variant ||= "btn btn-primary" %>
2
2
  <% type ||= "button" %>
3
3
  <% label ||= "Submit" %>
4
4
  <% icon ||= nil %>
@@ -11,7 +11,7 @@
11
11
  <div class="dropdown dropdown-menu-end position-relative" id="<%= dropdown_id %>_wrapper">
12
12
  <%= tag.button raw("#{icon_html}#{label}"),
13
13
  type: type,
14
- class: "btn kaui-dropdown custom-hover #{variant} #{html_class}",
14
+ class: "#{variant} #{html_class}",
15
15
  id: "#{dropdown_id}_button",
16
16
  aria: { expanded: "false" } %>
17
17
 
@@ -6,7 +6,7 @@
6
6
  <span class="icon-container">
7
7
  <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
8
8
  </span>
9
- Advance Search
9
+ Advanced Search
10
10
  </h5>
11
11
  <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
12
12
  <span aria-hidden="true">
@@ -35,6 +35,13 @@
35
35
  </div>
36
36
  <div id="search-fields-container">
37
37
  </div>
38
+ <div id="save-search-container" style="display: none;">
39
+ <hr class="mt-4 mb-3">
40
+ <div class="form-group d-flex align-items-center mb-0">
41
+ <label for="savedSearchName" class="mr-2 field-label" style="width: 30%;">Save As Name</label>
42
+ <input type="text" id="savedSearchName" class="form-control flex-grow-1" placeholder="Enter a name for this search...">
43
+ </div>
44
+ </div>
38
45
  </form>
39
46
  <% unless @account.account_id.present? %>
40
47
  <div class="alert alert-info" role="alert">
@@ -43,6 +50,15 @@
43
50
  <% end %>
44
51
  </div>
45
52
  <div class="modal-footer">
53
+ <%= render "kaui/components/button/button", {
54
+ label: 'Save Search As...',
55
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
56
+ type: "button",
57
+ html_class: "kaui-button custom-hover",
58
+ html_options: {
59
+ id: "saveAdvanceSearch"
60
+ }
61
+ } %>
46
62
  <%= render "kaui/components/button/button", {
47
63
  label: 'Clear Search',
48
64
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
@@ -92,6 +108,7 @@
92
108
 
93
109
  <%= javascript_tag do %>
94
110
  $(document).ready(function() {
111
+
95
112
  populateSearchLabelsFromUrl();
96
113
  var dateFields = ['Invoice date', 'Target date'];
97
114
  // Handle the "Add" button click to add new search fields
@@ -123,15 +140,36 @@ $(document).ready(function() {
123
140
  var searchLabelsContainer = $('#search-labels-container');
124
141
  searchLabelsContainer.empty();
125
142
 
143
+ // Validate that at least one search field has a value
144
+ var hasValue = false;
145
+ searchFields.each(function() {
146
+ var value = $(this).find('.search-field-value').val().trim();
147
+ if (value !== '') {
148
+ hasValue = true;
149
+ return false; // Break the loop
150
+ }
151
+ });
152
+
153
+ // If no search field has a value, show an alert and prevent search
154
+ if (!hasValue) {
155
+ alert('Please enter a value for at least one search field.');
156
+ return;
157
+ }
158
+
126
159
  var table = $('#invoices-table').DataTable();
127
- table.on('preXhr.dt', function(e, settings, data) {
160
+ table.off('preXhr.dt.filter');
161
+ table.on('preXhr.dt.filter', function(e, settings, data) {
128
162
  data.search.value = searchQuery("<%= @search_query.to_s %>");
129
163
  });
130
164
 
131
- table.ajax.url("<%= invoices_pagination_path(:ordering => @ordering, :format => :json) %>").load();
165
+ var searchParams = searchQuery("<%= @search_query.to_s %>");
166
+ var ajaxUrl = "<%= invoices_pagination_path(:ordering => @ordering, :format => :json) %>";
167
+ if (searchParams) {
168
+ ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + searchParams;
169
+ }
170
+ table.ajax.url(ajaxUrl).load();
132
171
 
133
172
  // Update the URL with the search parameters
134
- var searchParams = searchQuery("<%= @search_query.to_s %>");
135
173
  if (searchParams) {
136
174
  searchParams = searchParams.replace(/account_id/g, 'ac_id');
137
175
  var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + searchParams;
@@ -1,8 +1,8 @@
1
1
  <div class="d-flex">
2
2
  <div class="filter-bar-container">
3
- <div class="filter-bar">
3
+ <div class="filter-bar d-flex gap-2">
4
4
  <%= render "kaui/components/button/button", {
5
- label: "Advance Search",
5
+ label: "Advanced Search",
6
6
  icon: "kaui/search.svg",
7
7
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
8
8
  type: "button",
@@ -14,7 +14,13 @@
14
14
  }
15
15
  }
16
16
  } %>
17
-
17
+ <%= render "kaui/components/menu_dropdown/menu_dropdown", {
18
+ label: "Saved Searches",
19
+ icon: "kaui/save.svg",
20
+ variant: "btn btn-outline-secondary d-inline-flex align-items-center gap-1 kaui-button custom-hover",
21
+ dropdown_id: "savedSearchesDropdown",
22
+ menu_items: []
23
+ } %>
18
24
  </div>
19
25
  </div>
20
26
 
@@ -213,13 +219,6 @@ $(document).ready(function() {
213
219
  return new bootstrap.Dropdown(dropdownToggleEl);
214
220
  });
215
221
 
216
- // Manual dropdown toggle as fallback
217
- $('#dropdownMenu1').on('click', function(e) {
218
- e.preventDefault();
219
- e.stopPropagation();
220
- $('#column-visibility').toggleClass('show');
221
- });
222
-
223
222
  $('.dropdown-menu').on('click', 'input[type="checkbox"], label', function(event) {
224
223
  event.stopPropagation();
225
224
  });
@@ -50,7 +50,7 @@ $(document).ready(function() {
50
50
  }
51
51
  });
52
52
 
53
- var stateKey = 'DataTables_invoices-table_' + window.location.pathname.replace(/\//g, '_');
53
+ var stateKey = 'DataTables_invoices-table';
54
54
  var state = JSON.parse(localStorage.getItem(stateKey));
55
55
  if (state) {
56
56
  state.start = <%= @offset %>;
@@ -64,10 +64,10 @@ $(document).ready(function() {
64
64
  },
65
65
  "stateSave": true,
66
66
  "stateSaveCallback": function(settings, data) {
67
- localStorage.setItem('DataTables_invoices-table', JSON.stringify(data));
67
+ localStorage.setItem(stateKey, JSON.stringify(data));
68
68
  },
69
69
  "stateLoadCallback": function(settings) {
70
- return JSON.parse(localStorage.getItem('DataTables_invoices-table'));
70
+ return JSON.parse(localStorage.getItem(stateKey));
71
71
  },
72
72
  "scrollX": true,
73
73
  "dom": "<'row'r>t<'row'<'col-md-6'i><'col-md-6'p>>",
@@ -102,6 +102,18 @@ $(document).ready(function() {
102
102
  }
103
103
  });
104
104
 
105
+ // If the page loaded with advanced search params in the URL (e.g. restored from
106
+ // localStorage) but @advance_search_query was not set server-side, re-fire the
107
+ // DataTable request with the correct filters.
108
+ if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
109
+ var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
110
+ var ajaxUrl = "<%= invoices_pagination_path(:ordering => @ordering, :format => :json) %>" + '?' + urlSearch;
111
+ table.on('preXhr.dt.filter', function(e, settings, data) {
112
+ data.search.value = urlSearch;
113
+ });
114
+ table.ajax.url(ajaxUrl).load();
115
+ }
116
+
105
117
  // Custom sorting functionality
106
118
  var currentSortColumn = invoiceDateColIndex;
107
119
  var currentSortDirection = 'desc';
@@ -1,15 +1,15 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <%= render :template => 'kaui/layouts/kaui_header' %>
4
- <body>
5
- <% if user_signed_in? -%>
6
- <%= render :template => 'kaui/layouts/kaui_navbar' %>
7
- <% end %>
4
+ <body>
5
+ <% if user_signed_in? -%>
6
+ <%= render :template => 'kaui/layouts/kaui_navbar' %>
7
+ <% end %>
8
8
 
9
- <div class="container-fluid auth-container">
10
- <%= render :template => 'kaui/layouts/kaui_flash' %>
11
- <%= yield %>
12
- </div>
13
- <%= render :template => 'kaui/layouts/kaui_footer' %>
14
- </body>
9
+ <div class="container-fluid auth-container">
10
+ <%= render :template => 'kaui/layouts/kaui_flash' %>
11
+ <%= yield %>
12
+ </div>
13
+ <%= render :template => 'kaui/layouts/kaui_footer' %>
14
+ </body>
15
15
  </html>
@@ -12,28 +12,7 @@
12
12
  </div>
13
13
  </div>
14
14
  <% end %>
15
- <% end %>
16
-
17
-
18
- <% if defined?(@account) %>
19
- <% if @account.errors.any? %>
20
- <div class="server-alert kaui-container centered-absolute">
21
- <h3 class="error-title">The following errors prevented the account from being created:</h3>
22
- <div class="custom-alert custom-alert-danger">
23
- <i class="bi bi-exclamation-triangle-fill"></i>
24
- <ul>
25
- <% @account.errors.values.flatten.each do |msg| %>
26
- <li><%= msg %></li>
27
- <% end %>
28
- </ul>
29
- </div>
30
- </div>
31
15
  <% end %>
32
- <% end %>
33
-
34
-
35
-
36
-
37
16
 
38
17
  <% if flash[:warning] %>
39
18
  <div id="flash-warning" class="server-alert kaui-container centered-absolute">
@@ -89,21 +68,21 @@
89
68
  $('#flash-notice').fadeOut(500);
90
69
  }, 3000);
91
70
  });
92
-
71
+
93
72
  // Function to close flash messages
94
73
  function closeFlashMessage(type) {
95
74
  $('#flash-' + type).fadeOut(500);
96
75
  }
97
-
76
+
98
77
  // Function to show ajax info alert with auto-hide
99
78
  function ajaxInfo(message, timeout) {
100
79
  if (typeof timeout === 'undefined') {
101
80
  timeout = 3000; // Default to 3 seconds
102
81
  }
103
-
82
+
104
83
  $('#ajaxInfoAlertMessage').text(message);
105
84
  $('#ajaxInfoAlert').fadeIn(300);
106
-
85
+
107
86
  // Auto-hide after specified timeout
108
87
  setTimeout(function() {
109
88
  $('#ajaxInfoAlert').fadeOut(500);
@@ -118,4 +97,4 @@
118
97
  $('#ajaxInfoAlert').fadeOut(500);
119
98
  }
120
99
  }
121
- </script>
100
+ </script>
@@ -1,8 +1,8 @@
1
1
  <div class="d-flex">
2
2
  <div class="filter-bar-container">
3
- <div class="filter-bar">
3
+ <div class="filter-bar d-flex gap-2">
4
4
  <%= render "kaui/components/button/button", {
5
- label: "Advance Search",
5
+ label: "Advanced Search",
6
6
  icon: "kaui/search.svg",
7
7
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
8
8
  type: "button",
@@ -14,6 +14,13 @@
14
14
  }
15
15
  }
16
16
  } %>
17
+ <%= render "kaui/components/menu_dropdown/menu_dropdown", {
18
+ label: "Saved Searches",
19
+ icon: "kaui/save.svg",
20
+ variant: "btn btn-outline-secondary d-inline-flex align-items-center gap-1 kaui-button custom-hover",
21
+ dropdown_id: "savedSearchesDropdown",
22
+ menu_items: []
23
+ } %>
17
24
  </div>
18
25
  </div>
19
26
 
@@ -212,13 +219,6 @@ $(document).ready(function() {
212
219
  return new bootstrap.Dropdown(dropdownToggleEl);
213
220
  });
214
221
 
215
- // Manual dropdown toggle as fallback
216
- $('#dropdownMenu1').on('click', function(e) {
217
- e.preventDefault();
218
- e.stopPropagation();
219
- $('#column-visibility').toggleClass('show');
220
- });
221
-
222
222
  $('.dropdown-menu').on('click', 'input[type="checkbox"], label', function(event) {
223
223
  event.stopPropagation();
224
224
  });
@@ -7,7 +7,7 @@
7
7
  <span class="icon-container">
8
8
  <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
9
9
  </span>
10
- Advance Search
10
+ Advanced Search
11
11
  </h5>
12
12
  <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
13
13
  <span aria-hidden="true">
@@ -36,9 +36,25 @@
36
36
  </div>
37
37
  <div id="search-fields-container">
38
38
  </div>
39
+ <div id="save-search-container" style="display: none;">
40
+ <hr class="mt-4 mb-3">
41
+ <div class="form-group d-flex align-items-center mb-0">
42
+ <label for="savedSearchName" class="mr-2 field-label" style="width: 30%;">Save As Name</label>
43
+ <input type="text" id="savedSearchName" class="form-control flex-grow-1" placeholder="Enter a name for this search...">
44
+ </div>
45
+ </div>
39
46
  </form>
40
47
  </div>
41
48
  <div class="modal-footer">
49
+ <%= render "kaui/components/button/button", {
50
+ label: 'Save Search As...',
51
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
52
+ type: "button",
53
+ html_class: "kaui-button custom-hover",
54
+ html_options: {
55
+ id: "saveAdvanceSearch"
56
+ }
57
+ } %>
42
58
  <%= render "kaui/components/button/button", {
43
59
  label: 'Clear Search',
44
60
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
@@ -87,6 +103,7 @@
87
103
 
88
104
  <%= javascript_tag do %>
89
105
  $(document).ready(function() {
106
+
90
107
  populateSearchLabelsFromUrl();
91
108
 
92
109
  // Handle the "Add" button click to add new search fields
@@ -117,15 +134,36 @@ $(document).ready(function() {
117
134
  var searchLabelsContainer = $('#search-labels-container');
118
135
  searchLabelsContainer.empty();
119
136
 
137
+ // Validate that at least one search field has a value
138
+ var hasValue = false;
139
+ searchFields.each(function() {
140
+ var value = $(this).find('.search-field-value').val().trim();
141
+ if (value !== '') {
142
+ hasValue = true;
143
+ return false; // Break the loop
144
+ }
145
+ });
146
+
147
+ // If no search field has a value, show an alert and prevent search
148
+ if (!hasValue) {
149
+ alert('Please enter a value for at least one search field.');
150
+ return;
151
+ }
152
+
120
153
  var table = $('#payments-table').DataTable();
121
- table.on('preXhr.dt', function(e, settings, data) {
154
+ table.off('preXhr.dt.filter');
155
+ table.on('preXhr.dt.filter', function(e, settings, data) {
122
156
  data.search.value = searchQuery("<%= @search_query.to_s %>");
123
157
  });
124
158
 
125
- table.ajax.url("<%= payments_pagination_path(:ordering => @ordering, :format => :json) %>").load();
159
+ var searchParams = searchQuery("<%= @search_query.to_s %>");
160
+ var ajaxUrl = "<%= payments_pagination_path(:ordering => @ordering, :format => :json) %>";
161
+ if (searchParams) {
162
+ ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + searchParams;
163
+ }
164
+ table.ajax.url(ajaxUrl).load();
126
165
 
127
166
  // Update the URL with the search parameters
128
- var searchParams = searchQuery("<%= @search_query.to_s %>");
129
167
  if (searchParams) {
130
168
  searchParams = searchParams.replace(/account_id/g, 'ac_id');
131
169
  var newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + searchParams;
@@ -117,6 +117,18 @@ $(document).ready(function() {
117
117
  "search": {"search": "<%= @search_query %>"},
118
118
  });
119
119
 
120
+ // If the page loaded with advanced search params in the URL (e.g. restored from
121
+ // localStorage) but @advance_search_query was not set server-side, re-fire the
122
+ // DataTable request with the correct filters.
123
+ if (window.location.search.includes('_q=1') && '<%= j(@advance_search_query.to_s.strip) %>' === '' && !window.location.pathname.includes('/accounts/')) {
124
+ var urlSearch = window.location.search.substring(1).replace(/ac_id/g, 'account_id');
125
+ var ajaxUrl = "<%= payments_pagination_path(:ordering => @ordering, :format => :json) %>" + '?' + urlSearch;
126
+ table.on('preXhr.dt.filter', function(e, settings, data) {
127
+ data.search.value = urlSearch;
128
+ });
129
+ table.ajax.url(ajaxUrl).load();
130
+ }
131
+
120
132
  // Custom sorting functionality
121
133
  var currentSortColumn = -1;
122
134
  var currentSortDirection = 'asc';
@@ -50,6 +50,8 @@ en:
50
50
  clock_reset_successfully: 'Clock was successfully reset'
51
51
  overdue_uploaded_successfully: 'Overdue config was successfully uploaded'
52
52
  overdue_added_successfully: 'Overdue config was successfully added'
53
+ overdue_updated_successfully: 'Overdue config was successfully updated'
54
+ overdue_deleted_successfully: 'Overdue config was successfully deleted'
53
55
  invoice_template_uploaded_successfully: 'Invoice template was successfully uploaded'
54
56
  invoice_translation_uploaded_successfully: 'Invoice translation was successfully uploaded'
55
57
  catalog_translation_uploaded_successfully: 'Catalog translation was successfully uploaded'
@@ -84,3 +86,4 @@ en:
84
86
 
85
87
  admin_tenants:
86
88
  clock_warning: "This action will affect all tenants across the system. Proceed with caution."
89
+ 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
@@ -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.13'
4
+ VERSION = '4.0.15'
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.13
4
+ version: 4.0.15
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-15 00:00:00.000000000 Z
11
+ date: 2026-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -341,6 +341,7 @@ files:
341
341
  - app/assets/images/kaui/payment/payment.svg
342
342
  - app/assets/images/kaui/payment/refund.svg
343
343
  - app/assets/images/kaui/plus.svg
344
+ - app/assets/images/kaui/save.svg
344
345
  - app/assets/images/kaui/search.png
345
346
  - app/assets/images/kaui/search.svg
346
347
  - app/assets/images/kaui/search_white.png