kaui 4.0.13 → 4.0.14

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62d467e5b936f1738863675c8f5747e9ea4187ac28670556e09d857b1921d588
4
- data.tar.gz: c79e0a54c96cb563b1eaf7129f147240fd8da04e784a284f7390c39a7b5ac587
3
+ metadata.gz: e5d83c894b4705f72b548117b270bd7ea3c0d1fb962be8a49198dd81771c8c79
4
+ data.tar.gz: f95c3ff23e3b8729036252c2f45b2b2294bdb14dc474bf8666c188714bb29eff
5
5
  SHA512:
6
- metadata.gz: f6ff3a1e16012a78cfed10bfaf51ce9733f22d9ebf624d539cdf03cf3a8d7f8dcbda547022dc9258334ae9411c32a0261a7a81b40b19000218a0142950297198
7
- data.tar.gz: fa4d96b4ccb3afea63726fa9d6a2c2e0f5c853ce8ce5e5acf518c839ab0487fd9c5952ccf33d0b21307b3703ac48b917c15e486061628fb8462091280fb8d21c
6
+ metadata.gz: f0ea477030153cfad26f64b1e8840a72613362a6a0655cf34e5c91c53439d1b67953b2b2c76b4cc1622c171389bb1676b7aa742fc3b90ec3e21b4d897c343a66
7
+ data.tar.gz: 5f0a1131795c3072024624fc7c55aace8111e8ec65cbf2dc4878b200bf4e6019ef6bf826e4d54364145b2a904fec983fa5f77fdaa809f2dbef43e2cd73bf6895
@@ -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
+ $('#advanceSearchModal').modal('hide');
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
@@ -306,12 +306,18 @@ module Kaui
306
306
  options[:api_key] = current_tenant.api_key
307
307
  options[:api_secret] = current_tenant.api_secret
308
308
 
309
- view_form_model = params.require(:kill_bill_client_model_overdue).permit!.to_h.compact_blank
309
+ overdue_params = params[:kill_bill_client_model_overdue]&.permit!
310
+ view_form_model = overdue_params&.to_h&.compact_blank || {}
310
311
  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')
312
+ if view_form_model['states'].blank?
313
+ Kaui::AdminTenant.delete_tenant_user_key_value('OVERDUE_CONFIG', options[:username], nil, comment, options)
314
+ flash[:overdue_deleted] = true
315
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_updated_successfully')
316
+ else
317
+ overdue = Kaui::Overdue.from_overdue_form_model(view_form_model)
318
+ Kaui::Overdue.upload_tenant_overdue_config_json(overdue.to_json, options[:username], nil, comment, options)
319
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_added_successfully')
320
+ end
315
321
  end
316
322
 
317
323
  def upload_overdue_config
@@ -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>
@@ -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
  });
@@ -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,7 @@ 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'
53
54
  invoice_template_uploaded_successfully: 'Invoice template was successfully uploaded'
54
55
  invoice_translation_uploaded_successfully: 'Invoice translation was successfully uploaded'
55
56
  catalog_translation_uploaded_successfully: 'Catalog translation was successfully uploaded'
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.14'
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.14
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-04-28 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