kaui 4.0.12 → 4.0.13

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: a7cae94ec25bd96299f58f2bd783dabf34441f203d38fb2846e0f92b59b97e2a
4
- data.tar.gz: 04f9d02da92a77eb81160518cad11a5c88af312d20b1c65cfc0d3ad141814801
3
+ metadata.gz: 62d467e5b936f1738863675c8f5747e9ea4187ac28670556e09d857b1921d588
4
+ data.tar.gz: c79e0a54c96cb563b1eaf7129f147240fd8da04e784a284f7390c39a7b5ac587
5
5
  SHA512:
6
- metadata.gz: b99fa2fe8751882f22e754e79a1b1eacc1a7933a039e572bbfa22cf880d1967b7e8c8156d04d98cc6511c9b2b6ae634f9ba3ff24102bc4341072b2422dd41e68
7
- data.tar.gz: f494903ee370dd0a6bb10808bde638eedd769619cb7e0c70b321c74c9b246ea09ebc00f4d9976cfac8765c53a43fd724daace311ea59b15288d7adb070fb20cb
6
+ metadata.gz: f6ff3a1e16012a78cfed10bfaf51ce9733f22d9ebf624d539cdf03cf3a8d7f8dcbda547022dc9258334ae9411c32a0261a7a81b40b19000218a0142950297198
7
+ data.tar.gz: fa4d96b4ccb3afea63726fa9d6a2c2e0f5c853ce8ce5e5acf518c839ab0487fd9c5952ccf33d0b21307b3703ac48b917c15e486061628fb8462091280fb8d21c
@@ -302,6 +302,27 @@ jQuery(document).ready(function ($) {
302
302
 
303
303
  setObjectIdPopover();
304
304
  setObjectIdTooltip();
305
+
306
+ /*
307
+ * Tag dropdown overflow prevention
308
+ * Flips each .tag-select-box to open leftward when it would overflow the right edge of the viewport.
309
+ */
310
+ function repositionTagDropdowns() {
311
+ $('.tag-select').each(function() {
312
+ var $box = $(this).find('.tag-select-box');
313
+ var triggerRight = $(this).offset().left + $(this).outerWidth();
314
+ var boxWidth = Math.max($box[0].scrollWidth, 240);
315
+ var viewportWidth = $(window).width();
316
+ if (triggerRight + boxWidth > viewportWidth) {
317
+ $box.css({ left: 'auto', right: '0' });
318
+ } else {
319
+ $box.css({ left: '0', right: 'auto' });
320
+ }
321
+ });
322
+ }
323
+
324
+ repositionTagDropdowns();
325
+ $(window).on('resize', repositionTagDropdowns);
305
326
  });
306
327
 
307
328
 
@@ -689,4 +689,79 @@ table tr.expired td {
689
689
  line-height: 1.25rem;
690
690
  color: #414651;
691
691
  margin-bottom: 0.25rem;
692
+ }
693
+
694
+ /* Advance Search modal – Subscription Bundles page */
695
+ .subscription-bundl-index .close-button {
696
+ background: transparent;
697
+ padding: 0;
698
+ margin-top: -1.25rem;
699
+ }
700
+
701
+ .subscription-bundl-index .close-button:hover {
702
+ background: transparent;
703
+ padding: 0;
704
+ }
705
+
706
+ .subscription-bundl-index .border-button {
707
+ background: transparent;
708
+ display: inline-flex;
709
+ justify-content: center;
710
+ align-items: center;
711
+ border: 0.0625rem solid #D5D7DA;
712
+ border-radius: 0.375rem;
713
+ width: 2.5rem;
714
+ height: 2.5rem;
715
+ padding: 0.625rem;
716
+ }
717
+
718
+ .subscription-bundl-index .border-button:hover {
719
+ background: transparent;
720
+ }
721
+
722
+ .subscription-bundl-index .button {
723
+ background: transparent;
724
+ display: inline-flex;
725
+ justify-content: center;
726
+ align-items: center;
727
+ width: 2.5rem;
728
+ height: 2.5rem;
729
+ padding: 0.625rem;
730
+ }
731
+
732
+ .subscription-bundl-index .button:hover {
733
+ background: transparent;
734
+ }
735
+
736
+ .subscription-bundl-index .field-label {
737
+ font-weight: 500;
738
+ font-size: 0.875rem;
739
+ line-height: 1.25rem;
740
+ color: #414651;
741
+ margin-top: -1.25rem;
742
+ min-width: 9.59375rem;
743
+ }
744
+
745
+ .subscription-bundl-index .search-field-label {
746
+ margin-top: -1.25rem;
747
+ }
748
+
749
+ .subscription-bundl-index .form-group.row.align-items-center {
750
+ display: flex;
751
+ align-items: center;
752
+ }
753
+
754
+ .subscription-bundl-index .form-group.row.align-items-center label {
755
+ margin-bottom: 0;
756
+ }
757
+
758
+ .subscription-bundl-index .form-group.row.align-items-center .form-control {
759
+ margin-right: 0.25rem;
760
+ height: 2.5rem;
761
+ border-radius: 0.375rem;
762
+ }
763
+
764
+ .subscription-bundl-index .form-group.row.align-items-center.search-field div {
765
+ padding: 0;
766
+ margin-right: 0.25rem;
692
767
  }
@@ -58,15 +58,18 @@
58
58
  .tag-bar .tag-select .tag-select-box {
59
59
  position: absolute;
60
60
  top: 100%;
61
- left: -0.0625rem;
61
+ left: 0;
62
+ right: auto;
62
63
  max-height: 0;
63
64
  background-color: #fff;
64
65
  z-index: 100;
65
66
  padding: 0 0.9375rem;
66
67
  font-size: 0.75rem;
67
68
  color: #999999;
68
- overflow: auto;
69
- width: auto;
69
+ overflow: hidden;
70
+ min-width: 15rem;
71
+ width: max-content;
72
+ max-width: min(25rem, 95vw);
70
73
  border-bottom: 0;
71
74
  display: inline-block;
72
75
  }
@@ -5,7 +5,7 @@ module Kaui
5
5
  class AccountsController < Kaui::EngineController
6
6
  def index
7
7
  @search_query = params[:q]
8
- @advance_search_query = @search_query || request.query_string
8
+ @advance_search_query = @search_query.presence || params[:advance_search_query].presence
9
9
  if @search_query.present?
10
10
  account = Kaui::Account.list_or_search(@search_query, -1, 1, options_for_klient).first
11
11
  if account.nil?
@@ -288,7 +288,15 @@ 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 = Kaui::Overdue.get_overdue_json(options)
291
+ begin
292
+ @overdue = Kaui::Overdue.get_overdue_json(options)
293
+ @overdue_corrupted = false
294
+ rescue StandardError => e
295
+ Rails.logger.warn("Failed to load overdue configuration for tenant #{@tenant.id}: #{e.class}: #{e.message}")
296
+ @overdue = KillBillClient::Model::Overdue.new.tap { |o| o.overdue_states = [] }
297
+ @overdue_corrupted = true
298
+ 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
+ end
292
300
  end
293
301
 
294
302
  def modify_overdue_config
@@ -5,10 +5,11 @@ module Kaui
5
5
  # rubocop:disable Lint/HashCompareByIdentity
6
6
  def index
7
7
  cached_options_for_klient = options_for_klient
8
+ @search_query = params[:q].presence
9
+ @search_by = params[:search_by] || 'bundle_id'
8
10
  @per_page = (params[:per_page] || 10).to_i
9
11
  @page = (params[:page] || 1).to_i
10
12
 
11
- fetch_bundles = promise { Kaui::Account.paginated_bundles(@account.account_id, (@page - 1) * @per_page, @per_page, 'NONE', cached_options_for_klient) }
12
13
  fetch_bundle_tags = promise do
13
14
  all_bundle_tags = @account.all_tags(:BUNDLE, false, 'NONE', cached_options_for_klient)
14
15
  all_bundle_tags.each_with_object({}) do |entry, hsh|
@@ -36,8 +37,15 @@ module Kaui
36
37
  fetch_available_tags = promise { Kaui::TagDefinition.all_for_bundle(cached_options_for_klient) }
37
38
  fetch_available_subscription_tags = promise { Kaui::TagDefinition.all_for_subscription(cached_options_for_klient) }
38
39
 
39
- @bundles = wait(fetch_bundles)
40
- @total_pages = (@bundles.pagination_max_nb_records.to_f / @per_page).ceil
40
+ if @search_query.present?
41
+ @bundles = search_bundles(@search_query, @search_by, cached_options_for_klient)
42
+ @total_pages = 1
43
+ @page = 1
44
+ else
45
+ fetched = Kaui::Account.paginated_bundles(@account.account_id, (@page - 1) * @per_page, @per_page, 'NONE', cached_options_for_klient)
46
+ @bundles = fetched
47
+ @total_pages = (fetched.pagination_max_nb_records.to_f / @per_page).ceil
48
+ end
41
49
 
42
50
  @tags_per_bundle = wait(fetch_bundle_tags)
43
51
  @tags_per_subscription = wait(fetch_subscription_tags)
@@ -46,8 +54,15 @@ module Kaui
46
54
  @available_tags = wait(fetch_available_tags)
47
55
  @available_subscription_tags = wait(fetch_available_subscription_tags)
48
56
 
49
- # Don't load the full catalog to avoid memory issues
50
- @catalog = nil
57
+ # Collect the distinct start dates from subscriptions on this page, then fetch
58
+ # only the catalog versions needed — one per unique date — to avoid loading all
59
+ # historical versions into memory.
60
+ start_dates = @bundles.flat_map(&:subscriptions).filter_map(&:start_date).uniq
61
+ @catalogs = start_dates.filter_map do |date|
62
+ Kaui::Catalog.get_account_catalog_json(@account.account_id, date, cached_options_for_klient)&.last
63
+ rescue StandardError
64
+ nil
65
+ end.uniq(&:effective_date)
51
66
 
52
67
  @subscription = {}
53
68
  @bundles.each do |bundle|
@@ -112,5 +127,38 @@ module Kaui
112
127
  end
113
128
  redirect_to kaui_engine.account_bundles_path(@account.account_id), notice: msg
114
129
  end
130
+
131
+ private
132
+
133
+ def search_bundles(query, search_by, options)
134
+ case search_by
135
+ when 'bundle_id'
136
+ bundle = Kaui::Bundle.find_by_id(query, options)
137
+ bundle ? [bundle] : []
138
+ when 'bundle_external_key'
139
+ bundle = Kaui::Bundle.find_by_external_key(query, false, options)
140
+ bundle ? [bundle] : []
141
+ when 'subscription_id'
142
+ subscription = KillBillClient::Model::Subscription.find_by_id(query, 'NONE', options)
143
+ if subscription
144
+ bundle = Kaui::Bundle.find_by_id(subscription.bundle_id, options)
145
+ bundle ? [bundle] : []
146
+ else
147
+ []
148
+ end
149
+ when 'subscription_external_key'
150
+ subscription = KillBillClient::Model::Subscription.find_by_external_key(query, 'NONE', options)
151
+ if subscription
152
+ bundle = Kaui::Bundle.find_by_id(subscription.bundle_id, options)
153
+ bundle ? [bundle] : []
154
+ else
155
+ []
156
+ end
157
+ else
158
+ []
159
+ end
160
+ rescue KillBillClient::API::NotFound
161
+ []
162
+ end
115
163
  end
116
164
  end
@@ -127,7 +127,7 @@ module Kaui
127
127
  as_string(exception.cause)
128
128
  else
129
129
  log_rescue_error(exception)
130
- exception.message
130
+ exception.message.to_s[0..200]
131
131
  end
132
132
  end
133
133
 
@@ -150,7 +150,7 @@ module Kaui
150
150
  error_message += " (code=#{error_message['code']})" if error_message['code'].present?
151
151
  end
152
152
  # Limit the error size to avoid ActionDispatch::Cookies::CookieOverflow
153
- error_message[0..1000]
153
+ error_message.to_s[0..200]
154
154
  end
155
155
 
156
156
  def nested_hash_value(obj, key)
@@ -6,7 +6,7 @@ module Kaui
6
6
  def index
7
7
  @search_query = params[:account_id]
8
8
  @advance_search_query = params[:q] || request.query_string
9
- @ordering = params[:ordering] || (@search_query.blank? ? 'desc' : 'asc')
9
+ @ordering = params[:ordering] || 'desc'
10
10
  @offset = params[:offset] || 0
11
11
  @limit = params[:limit] || 50
12
12
  @search_fields = Kaui::Invoice::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
@@ -11,10 +11,8 @@ module Kaui
11
11
  def installed_plugin_names
12
12
  plugins = []
13
13
  nodes_info = KillBillClient::Model::NodesInfo.nodes_info(Kaui.current_tenant_user_options(current_user, session)) || []
14
- plugins_info = nodes_info.empty? ? [] : (nodes_info.first.plugins_info || [])
15
- plugins_info.each do |plugin|
16
- next unless plugin.state == 'RUNNING'
17
-
14
+ plugins_info = nodes_info.flat_map { |node| node.plugins_info || [] }
15
+ plugins_info.select { |p| p.state == 'RUNNING' }.uniq(&:plugin_name).each do |plugin|
18
16
  plugin_name = plugin.plugin_name
19
17
  plugin_key = plugin_name.gsub('-plugin', '')
20
18
 
@@ -40,7 +38,7 @@ module Kaui
40
38
  def installed_plugins
41
39
  installed_plugins = []
42
40
  nodes_info = KillBillClient::Model::NodesInfo.nodes_info(Kaui.current_tenant_user_options(current_user, session)) || []
43
- plugins_info = nodes_info.empty? ? [] : (nodes_info.first.plugins_info || [])
41
+ plugins_info = nodes_info.flat_map { |node| node.plugins_info || [] }
44
42
 
45
43
  plugins_info.each do |plugin|
46
44
  next if plugin.version.nil?
@@ -107,6 +107,24 @@ module Kaui
107
107
  end
108
108
  end
109
109
 
110
+ def catalog_for_subscription(sub, catalogs)
111
+ return nil if catalogs.blank? || sub&.start_date.blank?
112
+
113
+ start_date = Date.parse(sub.start_date)
114
+
115
+ # Find the latest catalog version whose effective_date <= subscription start_date
116
+ best = catalogs.select do |c|
117
+ Date.parse(c.effective_date) <= start_date
118
+ rescue StandardError
119
+ false
120
+ end.max_by(&:effective_date)
121
+
122
+ # Fall back to the oldest version if none precedes the start_date
123
+ best || catalogs.first
124
+ rescue StandardError
125
+ catalogs.last
126
+ end
127
+
110
128
  def humanized_subscription_phase_type(sub)
111
129
  sub.phase_type
112
130
  end
@@ -2,6 +2,12 @@
2
2
 
3
3
  module Kaui
4
4
  class Bundle < KillBillClient::Model::Bundle
5
+ SEARCH_FIELDS = [
6
+ ['bundle_id', 'Bundle ID'],
7
+ ['bundle_external_key', 'Bundle External Key'],
8
+ ['subscription_id', 'Subscription ID'],
9
+ ['subscription_external_key', 'Subscription External Key']
10
+ ].freeze
5
11
  def self.find_by_id_or_key(bundle_id_or_key, options = {})
6
12
  if /[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}/.match?(bundle_id_or_key)
7
13
  bundle = begin
@@ -37,8 +37,11 @@ module Kaui
37
37
 
38
38
  result.overdue_states << state
39
39
  end
40
- # We reversed them to display on the form , so we have to reverse them back before uploading new config
41
- result.overdue_states.reverse!
40
+ # Sort by days descending (most severe first) so Kill Bill receives them in the correct order
41
+ # regardless of the order the user added rows in the form.
42
+ result.overdue_states.sort_by! do |s|
43
+ -s.condition&.time_since_earliest_unpaid_invoice_equals_or_exceeds&.number.to_i
44
+ end
42
45
 
43
46
  result
44
47
  end
@@ -45,7 +45,7 @@
45
45
  <% end %>
46
46
  <span class="tag-bar tag-bar-breathe">
47
47
  <% unless @available_tags.blank? %>
48
- <div class="tag-select" onclick="void(0)">
48
+ <div class="tag-select position-relative" onclick="void(0)">
49
49
  <%= render "kaui/components/button/button", {
50
50
  label: "Tag As",
51
51
  trailing_icon: "kaui/account/down-arrow.svg",
@@ -61,7 +61,7 @@
61
61
  <td><%= state_form.select :is_block_changes, options_for_select([true, false ], state.is_block_changes), :class => 'form-control' %></td>
62
62
  <td><%= state_form.select :subscription_cancellation_policy, options_for_select([:NONE, :POLICY_NONE, :POLICY_IMMEDIATE, :POLICY_END_OF_TERM], state.subscription_cancellation), :class => 'form-control' %></td>
63
63
  <%= state_form.fields_for 'condition' do |condition| %>
64
- <td><%= condition.number_field :time_since_earliest_unpaid_invoice_equals_or_exceeds, :value => state.condition.time_since_earliest_unpaid_invoice_equals_or_exceeds&.number %></td>
64
+ <td><%= condition.number_field :time_since_earliest_unpaid_invoice_equals_or_exceeds, :value => state.condition.time_since_earliest_unpaid_invoice_equals_or_exceeds&.number, :min => 1, :class => 'days-since-field' %></td>
65
65
  <td><%= condition.select :control_tag_inclusion, options_for_select([:NONE, :AUTO_PAY_OFF, :AUTO_INVOICING_OFF, :OVERDUE_ENFORCEMENT_OFF, :MANUAL_PAY, :TEST, :PARTNER], state.condition&.control_tag_inclusion), :class => 'form-control' %></td>
66
66
  <td><%= condition.select :control_tag_exclusion, options_for_select([:NONE, :AUTO_PAY_OFF, :AUTO_INVOICING_OFF, :OVERDUE_ENFORCEMENT_OFF, :MANUAL_PAY, :TEST, :PARTNER], state.condition&.control_tag_exclusion), :class => 'form-control'%></td>
67
67
  <td><%= condition.number_field :number_of_unpaid_invoices_equals_or_exceeds, :value => state.condition&.number_of_unpaid_invoices_equals_or_exceeds %></td>
@@ -214,8 +214,32 @@ function overdue_delete_state(obj) {
214
214
  $("#tr_state_" + idx).hide();
215
215
  };
216
216
 
217
+ $('form.form-horizontal').on('submit', function(e) {
218
+ var invalid = false;
219
+ $('#existing-overdue-config-for-tenants tbody tr:visible').each(function() {
220
+ var $days = $(this).find('.days-since-field');
221
+ var val = parseInt($days.val(), 10);
222
+ if (isNaN(val) || val <= 0) {
223
+ $days.addClass('is-invalid');
224
+ invalid = true;
225
+ } else {
226
+ $days.removeClass('is-invalid');
227
+ }
228
+ });
229
+ if (invalid) {
230
+ e.preventDefault();
231
+ alert('"Days since earliest unpaid invoice" must be greater than 0 for all states.');
232
+ }
233
+ });
234
+
217
235
  $(document).ready(function() {
236
+ <% if @overdue_corrupted %>
237
+ switch_overdue_xml_config();
238
+ document.querySelectorAll('.toggle-option').forEach(opt => opt.classList.remove('active-btn'));
239
+ document.querySelectorAll('.toggle-option')[1]?.classList.add('active-btn');
240
+ <% else %>
218
241
  switch_overdue_basic_config();
242
+ <% end %>
219
243
  });
220
244
 
221
245
  document.querySelectorAll('.toggle-option').forEach(el => {
@@ -0,0 +1,119 @@
1
+ <div class="modal fade" id="advanceSearchModal" tabindex="-1" role="dialog" aria-labelledby="advanceSearchModalLabel" aria-hidden="true">
2
+ <div class="modal-dialog" role="document">
3
+ <div class="modal-content">
4
+ <div class="modal-header">
5
+ <h5 class="modal-title d-flex align-items-center gap-3" id="advanceSearchModalLabel">
6
+ <span class="icon-container">
7
+ <%= image_tag("kaui/modal/search.svg", width: 20, height: 20) %>
8
+ </span>
9
+ Advance Search
10
+ </h5>
11
+ <button type="button" class="close close-button custom-hover" data-bs-dismiss="modal" aria-label="Close">
12
+ <span aria-hidden="true">
13
+ <%= image_tag("kaui/modal/close.svg", width: 20, height: 20) %>
14
+ </span>
15
+ </button>
16
+ </div>
17
+ <div class="modal-body">
18
+ <form id="advanceSearchForm">
19
+ <div class="form-group d-flex align-items-center">
20
+ <label for="searchFieldSelect" class="mr-2 field-label" style="width: 30%;">Search Field</label>
21
+ <select id="searchFieldSelect" class="form-control mr-2">
22
+ <% Kaui::Bundle::SEARCH_FIELDS.each do |value, title| %>
23
+ <option value="<%= value %>" <%= 'selected' if @search_by == value %>><%= title %></option>
24
+ <% end %>
25
+ </select>
26
+ </div>
27
+ <div class="form-group d-flex align-items-center mt-3">
28
+ <label for="bundleSearchValue" class="mr-2 field-label" style="width: 30%;">Value</label>
29
+ <input type="text" id="bundleSearchValue" class="form-control" placeholder="Enter search value..." value="<%= @search_query %>">
30
+ </div>
31
+ </form>
32
+ </div>
33
+ <div class="modal-footer">
34
+ <%= render "kaui/components/button/button", {
35
+ label: 'Clear Search',
36
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
37
+ type: "button",
38
+ html_class: "kaui-button custom-hover",
39
+ html_options: { id: "clearAdvanceSearch" }
40
+ } %>
41
+ <%= render "kaui/components/button/button", {
42
+ label: 'Apply Search',
43
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
44
+ type: "button",
45
+ html_class: "kaui-dropdown custom-hover",
46
+ html_options: { id: "applyAdvanceSearch" }
47
+ } %>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <%= javascript_tag do %>
54
+ $(document).ready(function() {
55
+ // Pre-populate modal from current URL params on open
56
+ $('#advanceSearchModal').on('show.bs.modal', function() {
57
+ var params = new URLSearchParams(window.location.search);
58
+ var searchBy = params.get('search_by') || 'bundle_id';
59
+ var q = params.get('q') || '';
60
+ $('#searchFieldSelect').val(searchBy);
61
+ $('#bundleSearchValue').val(q);
62
+ });
63
+
64
+ // Apply search: navigate to same page with query params
65
+ $('#applyAdvanceSearch').on('click', function() {
66
+ var searchBy = $('#searchFieldSelect').val();
67
+ var q = $('#bundleSearchValue').val().trim();
68
+ if (!q) return;
69
+
70
+ var url = new URL(window.location.href);
71
+ url.searchParams.set('search_by', searchBy);
72
+ url.searchParams.set('q', q);
73
+ url.searchParams.delete('page');
74
+ window.location.href = url.toString();
75
+ });
76
+
77
+ // Clear search: navigate to same page without query params
78
+ $('#clearAdvanceSearch').on('click', function() {
79
+ var url = new URL(window.location.href);
80
+ url.searchParams.delete('q');
81
+ url.searchParams.delete('search_by');
82
+ url.searchParams.delete('page');
83
+ window.location.href = url.toString();
84
+ });
85
+
86
+ // Handle click on active search label close icon
87
+ $(document).on('click', '.bundle-filter-close-icon', function() {
88
+ var url = new URL(window.location.href);
89
+ url.searchParams.delete('q');
90
+ url.searchParams.delete('search_by');
91
+ url.searchParams.delete('page');
92
+ window.location.href = url.toString();
93
+ });
94
+
95
+ // Show active search label on page load
96
+ updateBundleSearchLabels();
97
+
98
+ function updateBundleSearchLabels() {
99
+ var params = new URLSearchParams(window.location.search);
100
+ var q = params.get('q');
101
+ var searchBy = params.get('search_by');
102
+ var container = $('#search-labels-container');
103
+ container.empty();
104
+
105
+ if (q && searchBy) {
106
+ var fieldLabel = $('#searchFieldSelect option[value="' + searchBy + '"]').text() || searchBy;
107
+ var label = $('<span>', {
108
+ class: 'label label-info d-inline-flex align-items-center gap-2'
109
+ });
110
+ label.append($('<span>', { text: fieldLabel + ': ' + q }));
111
+ label.append($('<span>', {
112
+ class: 'bundle-filter-close-icon',
113
+ style: 'cursor: pointer; margin-left: 5px; display: inline-flex; align-items: center;'
114
+ }).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>'));
115
+ container.append(label);
116
+ }
117
+ }
118
+ });
119
+ <% end %>
@@ -1,5 +1,6 @@
1
1
  <div class="kaui-container subscription-bundl-index pb-5">
2
2
  <%= render "kaui/components/breadcrumb/breadcrumb" %>
3
+ <%= render :partial => 'bundle_filterbar' %>
3
4
  <div class="d-flex mb-5" style="gap: 4rem;">
4
5
  <%= render :template => 'kaui/layouts/kaui_account_sidebar' %>
5
6
  <div class="subscription-bundle">
@@ -8,7 +9,20 @@
8
9
  <div class="d-flex align-items-center">
9
10
  <h2>Subscription Bundles</h2>
10
11
  </div>
11
- <span>
12
+ <span class="d-flex align-items-center gap-2">
13
+ <%= render "kaui/components/button/button", {
14
+ label: "Advance Search",
15
+ icon: "kaui/search.svg",
16
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
17
+ type: "button",
18
+ html_class: "kaui-button custom-hover",
19
+ html_options: {
20
+ data: {
21
+ bs_toggle: "modal",
22
+ bs_target: "#advanceSearchModal"
23
+ }
24
+ }
25
+ } %>
12
26
  <% if can? :create, Kaui::Subscription %>
13
27
  <%= link_to kaui_engine.new_subscription_path(:params => { :account_id => @account.account_id, :product_category => 'BASE' }) do %>
14
28
  <%= render "kaui/components/button/button", {
@@ -22,21 +36,31 @@
22
36
  <% end %>
23
37
  </span>
24
38
  </div>
39
+ <div id="search-labels-container" class="ml-2 mb-2">
40
+ <!-- Dynamic search labels will be added here -->
41
+ </div>
25
42
  <div class="subscriptions-scroll">
26
43
  <% @bundles.each_with_index do |bundle, idx| %>
27
44
  <div class="row">
28
45
  <div class="d-flex">
29
- <%= render :partial => Kaui.bundle_details_partial, :locals => { :bundle => bundle, :account => @account, :catalog => @catalog } %>
46
+ <%= render :partial => Kaui.bundle_details_partial, :locals => { :bundle => bundle, :account => @account, :catalog => @catalogs&.last } %>
30
47
  </div>
31
48
  <% if bundle.subscriptions.present? %>
32
49
  <div class="search">
33
- <%= render :partial => 'kaui/subscriptions/subscriptions_table', :locals => {:bundle => bundle, :account => @account, :catalog => @catalog} %>
50
+ <%= render :partial => 'kaui/subscriptions/subscriptions_table', :locals => {:bundle => bundle, :account => @account, :catalogs => @catalogs} %>
34
51
  </div>
35
52
  <% end %>
36
53
  </div>
37
54
  <% end %>
38
55
 
39
56
 
57
+ <% if @bundles.empty? %>
58
+ <div class="custom-alert custom-alert-info mt-3">
59
+ <span>No bundles found<%= @search_query.present? ? " for the given search query." : "." %></span>
60
+ </div>
61
+ <% end %>
62
+
63
+ <% unless @search_query.present? %>
40
64
  <div class="text-right d-flex justify-content-end pagination" style="">
41
65
  <%= link_to account_bundles_path(page: @page - 1), class: "btn btn-custom #{'disabled' if @page == 1}" do %>
42
66
  <%= render "kaui/components/button/button", {
@@ -81,6 +105,7 @@
81
105
  } %>
82
106
  <% end %>
83
107
  </div>
108
+ <% end %>
84
109
  </div>
85
110
  </div>
86
111
  </div>
@@ -41,7 +41,16 @@
41
41
 
42
42
  <%= javascript_tag do %>
43
43
  $(document).ready(function() {
44
- var stateKey = 'DataTables_invoices-table';
44
+ // Dynamically find the Invoice date column index based on current header order
45
+ var invoiceDateColIndex = 3; // fallback default
46
+ $('#invoices-table thead th').each(function(i) {
47
+ if ($(this).find('.header-text').text().trim().toLowerCase() === 'invoice date') {
48
+ invoiceDateColIndex = i;
49
+ return false;
50
+ }
51
+ });
52
+
53
+ var stateKey = 'DataTables_invoices-table_' + window.location.pathname.replace(/\//g, '_');
45
54
  var state = JSON.parse(localStorage.getItem(stateKey));
46
55
  if (state) {
47
56
  state.start = <%= @offset %>;
@@ -69,11 +78,8 @@ $(document).ready(function() {
69
78
  },
70
79
  "pageLength": <%= @limit %>,
71
80
  "displayStart": <%= @offset %>,
72
- <% if @search_query.blank? %>
73
- "ordering": true,
74
- <% elsif !@ordering.blank? %>
75
- "order": [[ 0, "<%= @ordering %>" ]],
76
- <% end %>
81
+ "ordering": true,
82
+ "order": [[ invoiceDateColIndex, "asc" ]],
77
83
  "processing": true,
78
84
  "serverSide": true,
79
85
  "search": {"search": "<%= @search_query %>"},
@@ -97,8 +103,8 @@ $(document).ready(function() {
97
103
  });
98
104
 
99
105
  // Custom sorting functionality
100
- var currentSortColumn = -1;
101
- var currentSortDirection = 'asc';
106
+ var currentSortColumn = invoiceDateColIndex;
107
+ var currentSortDirection = 'desc';
102
108
 
103
109
  // Handle custom header clicks
104
110
  $('.sortable-header').on('click', function() {
@@ -134,9 +140,7 @@ $(document).ready(function() {
134
140
 
135
141
  // Initialize sort indicators based on current state
136
142
  <% if !@ordering.blank? %>
137
- var initialColumn = 0;
138
- var initialDirection = "<%= @ordering %>";
139
- updateSortIndicators(initialColumn, initialDirection);
143
+ updateSortIndicators(invoiceDateColIndex, 'desc');
140
144
  <% end %>
141
145
 
142
146
  // Add an action whenever the page changes
@@ -126,8 +126,7 @@
126
126
  };
127
127
 
128
128
  var validateRefundAmount = function() {
129
- if (Number($("#refund_amount").attr('value')) > <%= @payment.purchased_amount %> ||
130
- Number($("#refund_amount").attr('value')) <= 0) {
129
+ if (Number($("#refund_amount").val()) <= 0) {
131
130
  setClassForElement("#div_refund_amount", "form-group d-flex pb-3 error");
132
131
  $('#new_kill_bill_client_model_invoice_item :submit').prop('disabled', true);
133
132
  } else {
@@ -151,11 +150,6 @@
151
150
  });
152
151
  };
153
152
 
154
- /*
155
- * Recompute refund amount based on adjustment type:
156
- * - For Invoice Item Adjustment, recompute price based on selection and invalidate text area to make it match exact selection
157
- * _ For Invoice adjustment or no adjustment, default to payment amount
158
- */
159
153
  var recomputeRefundAmountAndValidateAmount = function() {
160
154
  var computedRefundAmount = <%= @payment.purchased_amount %>;
161
155
  if ($("#adjustment_type_invoiceItemAdjustment").is(':checked')) {
@@ -166,11 +160,9 @@
166
160
  }
167
161
  });
168
162
  computedRefundAmount = x.toFixed(2);
169
- $("#refund_amount").attr('value', computedRefundAmount);
170
- $("#refund_amount").prop('readonly', true);
163
+ $("#refund_amount").val(computedRefundAmount);
171
164
  } else {
172
- $("#refund_amount").attr('value', computedRefundAmount);
173
- $("#refund_amount").prop('readonly', false);
165
+ $("#refund_amount").val(computedRefundAmount);
174
166
  }
175
167
  validateRefundAmount();
176
168
  };
@@ -104,7 +104,7 @@
104
104
  </div>
105
105
  </td>
106
106
  <% end %>
107
- <td><%= humanized_subscription_plan_or_product_name(sub, catalog) %></td>
107
+ <td><%= humanized_subscription_plan_or_product_name(sub, catalog_for_subscription(sub, catalogs)) %></td>
108
108
  <td>
109
109
  <span id="<%= sub.subscription_id %>-popover" class="object-id-popover category-bedge" data-id="<%= sub.subscription_id %>">
110
110
  <%= humanized_subscription_product_category(sub) %>
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.12'
4
+ VERSION = '4.0.13'
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.12
4
+ version: 4.0.13
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-01 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -576,6 +576,7 @@ files:
576
576
  - app/views/kaui/bundle_tags/_form_bar.html.erb
577
577
  - app/views/kaui/bundle_tags/edit.html.erb
578
578
  - app/views/kaui/bundles/_bundle_details.html.erb
579
+ - app/views/kaui/bundles/_bundle_filterbar.html.erb
579
580
  - app/views/kaui/bundles/index.html.erb
580
581
  - app/views/kaui/bundles/pause_resume.html.erb
581
582
  - app/views/kaui/bundles/transfer.html.erb