kaui 4.0.11 → 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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/kaui/kaui_override.js +21 -0
  3. data/app/assets/stylesheets/kaui/subscription.css +75 -0
  4. data/app/assets/stylesheets/kaui/tags.css +6 -3
  5. data/app/controllers/kaui/account_timelines_controller.rb +3 -3
  6. data/app/controllers/kaui/accounts_controller.rb +52 -52
  7. data/app/controllers/kaui/admin_allowed_users_controller.rb +29 -29
  8. data/app/controllers/kaui/admin_controller.rb +2 -2
  9. data/app/controllers/kaui/admin_tenants_controller.rb +89 -81
  10. data/app/controllers/kaui/audit_logs_controller.rb +1 -1
  11. data/app/controllers/kaui/bundles_controller.rb +53 -5
  12. data/app/controllers/kaui/chargebacks_controller.rb +1 -1
  13. data/app/controllers/kaui/charges_controller.rb +5 -2
  14. data/app/controllers/kaui/credits_controller.rb +1 -1
  15. data/app/controllers/kaui/custom_fields_controller.rb +11 -11
  16. data/app/controllers/kaui/engine_controller_util.rb +5 -5
  17. data/app/controllers/kaui/home_controller.rb +6 -2
  18. data/app/controllers/kaui/invoices_controller.rb +6 -4
  19. data/app/controllers/kaui/payment_methods_controller.rb +5 -5
  20. data/app/controllers/kaui/payments_controller.rb +19 -19
  21. data/app/controllers/kaui/queues_controller.rb +6 -6
  22. data/app/controllers/kaui/registrations_controller.rb +1 -1
  23. data/app/controllers/kaui/role_definitions_controller.rb +2 -2
  24. data/app/controllers/kaui/sessions_controller.rb +1 -1
  25. data/app/controllers/kaui/subscriptions_controller.rb +15 -14
  26. data/app/controllers/kaui/tag_definitions_controller.rb +1 -1
  27. data/app/controllers/kaui/tenants_controller.rb +2 -2
  28. data/app/controllers/kaui/transactions_controller.rb +6 -6
  29. data/app/helpers/kaui/account_helper.rb +9 -7
  30. data/app/helpers/kaui/exception_helper.rb +7 -5
  31. data/app/helpers/kaui/payment_helper.rb +2 -9
  32. data/app/helpers/kaui/plugin_helper.rb +3 -5
  33. data/app/helpers/kaui/subscription_helper.rb +46 -30
  34. data/app/helpers/kaui/uuid_helper.rb +1 -1
  35. data/app/models/kaui/account.rb +4 -3
  36. data/app/models/kaui/admin_tenant.rb +2 -2
  37. data/app/models/kaui/allowed_user.rb +3 -1
  38. data/app/models/kaui/allowed_user_tenant.rb +2 -2
  39. data/app/models/kaui/audit_log.rb +1 -1
  40. data/app/models/kaui/bundle.rb +11 -5
  41. data/app/models/kaui/invoice.rb +1 -1
  42. data/app/models/kaui/invoice_payment.rb +2 -2
  43. data/app/models/kaui/killbill_authenticatable.rb +2 -39
  44. data/app/models/kaui/killbill_registerable.rb +3 -11
  45. data/app/models/kaui/overdue.rb +5 -2
  46. data/app/models/kaui/payment.rb +1 -1
  47. data/app/models/kaui/payment_state.rb +3 -1
  48. data/app/models/kaui/rails_methods.rb +2 -2
  49. data/app/models/kaui/tag_definition.rb +2 -2
  50. data/app/models/kaui/tenant.rb +2 -1
  51. data/app/models/kaui/transaction.rb +1 -1
  52. data/app/services/dependencies/kenui.rb +1 -1
  53. data/app/views/kaui/accounts/_account_details.html.erb +1 -1
  54. data/app/views/kaui/admin_tenants/new_overdue_config.html.erb +25 -1
  55. data/app/views/kaui/bundles/_bundle_filterbar.html.erb +119 -0
  56. data/app/views/kaui/bundles/index.html.erb +28 -3
  57. data/app/views/kaui/components/search_input/_search_input.html.erb +39 -1
  58. data/app/views/kaui/invoices/index.html.erb +15 -11
  59. data/app/views/kaui/refunds/_form.html.erb +3 -11
  60. data/app/views/kaui/subscriptions/_form.html.erb +7 -1
  61. data/app/views/kaui/subscriptions/_subscriptions_table.html.erb +12 -1
  62. data/config/routes.rb +79 -79
  63. data/lib/devise/models/killbill_authenticatable.rb +38 -0
  64. data/lib/devise/models/killbill_registerable.rb +9 -0
  65. data/lib/generators/kaui/install/install_generator.rb +1 -4
  66. data/lib/kaui/version.rb +1 -1
  67. data/lib/kaui.rb +7 -7
  68. metadata +5 -2
@@ -2,8 +2,14 @@
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
- if bundle_id_or_key =~ /[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}/
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
8
14
  find_by_id(bundle_id_or_key, options)
9
15
  rescue StandardError
@@ -47,15 +53,15 @@ module Kaui
47
53
  end
48
54
 
49
55
  def self.list_transfer_policy_params
50
- @policy_params = [
56
+ [
51
57
  # [I18n.translate('start_of_term'), 'START_OF_TERM'], Temporarily removed as it is not supported by Kill Bill
52
- [I18n.translate('end_of_term'), 'END_OF_TERM'],
53
- [I18n.translate('immediate'), 'IMMEDIATE']
58
+ [I18n.t('end_of_term'), 'END_OF_TERM'],
59
+ [I18n.t('immediate'), 'IMMEDIATE']
54
60
  ]
55
61
  end
56
62
 
57
63
  def self.list_transfer_policy_params_keys
58
- @policy_params = %w[END_OF_TERM IMMEDIATE]
64
+ %w[END_OF_TERM IMMEDIATE]
59
65
  end
60
66
  end
61
67
  end
@@ -8,7 +8,7 @@ module Kaui
8
8
 
9
9
  def self.build_from_raw_invoice(raw_invoice)
10
10
  result = Kaui::Invoice.new
11
- KillBillClient::Model::InvoiceAttributes.instance_variable_get('@json_attributes').each do |attr|
11
+ KillBillClient::Model::InvoiceAttributes.instance_variable_get(:@json_attributes).each do |attr|
12
12
  result.send("#{attr}=", raw_invoice.send(attr))
13
13
  end
14
14
  result
@@ -17,7 +17,7 @@ module Kaui
17
17
  return nil if raw_payment.nil?
18
18
 
19
19
  result = Kaui::InvoicePayment.new
20
- KillBillClient::Model::InvoicePaymentAttributes.instance_variable_get('@json_attributes').each do |attr|
20
+ KillBillClient::Model::InvoicePaymentAttributes.instance_variable_get(:@json_attributes).each do |attr|
21
21
  result.send("#{attr}=", raw_payment.send(attr))
22
22
  end
23
23
  # Use Kaui::Transaction to benefit from additional fields (e.g next_retry_date)
@@ -25,7 +25,7 @@ module Kaui
25
25
  result.transactions = []
26
26
  original_transactions.each do |transaction|
27
27
  new_transaction = Kaui::Transaction.new
28
- KillBillClient::Model::PaymentTransactionAttributes.instance_variable_get('@json_attributes').each do |attr|
28
+ KillBillClient::Model::PaymentTransactionAttributes.instance_variable_get(:@json_attributes).each do |attr|
29
29
  new_transaction.send("#{attr}=", transaction.send(attr))
30
30
  end
31
31
  result.transactions << new_transaction
@@ -1,45 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'killbill_client'
3
+ require 'devise/models/killbill_authenticatable'
4
4
 
5
- # Hack for Zeitwerk
6
- # rubocop:disable Style/OneClassPerFile
5
+ # Placeholder for Zeitwerk - must define Kaui::KillbillAuthenticatable before Devise::Models::KillbillAuthenticatable
7
6
  module Kaui
8
7
  module KillbillAuthenticatable; end
9
8
  end
10
-
11
- module Devise
12
- module Models
13
- module KillbillAuthenticatable
14
- extend ActiveSupport::Concern
15
-
16
- def valid_killbill_password?(creds)
17
- # Simply try to look-up the permissions for that user - this will
18
- # Take care of the auth part
19
- response = Kaui::User.find_permissions(creds)
20
- # Auth was successful, update the session id
21
- self.kb_session_id = response.session_id
22
- true
23
- rescue KillBillClient::API::Unauthorized => _e
24
- false
25
- end
26
-
27
- def after_killbill_authentication
28
- save(validate: false)
29
- end
30
-
31
- module ClassMethods
32
- # Invoked by the KillbillAuthenticatable strategy to lookup the user
33
- # before attempting authentication
34
- def find_for_killbill_authentication(kb_username)
35
- find_for_authentication(kb_username:) ||
36
- new(kb_username:)
37
- rescue KillBillClient::API::Unauthorized => _e
38
- # Multi-Tenancy was enabled, but the tenant_id couldn't be retrieved because of bad credentials
39
- nil
40
- end
41
- end
42
- end
43
- end
44
- end
45
- # rubocop:enable Style/OneClassPerFile
@@ -1,16 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Hack for Zeitwerk
4
- # rubocop:disable Style/OneClassPerFile
3
+ require 'devise/models/killbill_registerable'
4
+
5
+ # Placeholder for Zeitwerk - must define Kaui::KillbillRegisterable before Devise::Models::KillbillRegisterable
5
6
  module Kaui
6
7
  module KillbillRegisterable; end
7
8
  end
8
-
9
- module Devise
10
- module Models
11
- module KillbillRegisterable
12
- include Registerable
13
- end
14
- end
15
- end
16
- # rubocop:enable Style/OneClassPerFile
@@ -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
@@ -19,7 +19,7 @@ module Kaui
19
19
 
20
20
  def self.build_from_raw_payment(raw_payment)
21
21
  result = Kaui::Payment.new
22
- KillBillClient::Model::PaymentAttributes.instance_variable_get('@json_attributes').each do |attr|
22
+ KillBillClient::Model::PaymentAttributes.instance_variable_get(:@json_attributes).each do |attr|
23
23
  result.send("#{attr}=", raw_payment.send(attr))
24
24
  end
25
25
  result
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Kaui
4
4
  module PaymentState
5
+ REFUNDABLE_TRANSACTION_TYPES = %w[CAPTURE PURCHASE].freeze
6
+
5
7
  def refundable?
6
8
  transactions.each do |transaction|
7
- return true if transaction.status == 'SUCCESS' && %w[CAPTURE PURCHASE].include?(transaction.transaction_type)
9
+ return true if transaction.status == 'SUCCESS' && REFUNDABLE_TRANSACTION_TYPES.include?(transaction.transaction_type)
8
10
  end
9
11
  false
10
12
  end
@@ -32,7 +32,7 @@ module Kaui
32
32
  send(attr)
33
33
  end
34
34
 
35
- # rubocop:disable Naming/PredicateMethod
35
+ # rubocop:disable Naming/PredicateMethod, ThreadSafety/ClassInstanceVariable
36
36
  def save
37
37
  @errors.add(:save, 'Saving this object is not yet supported')
38
38
  false
@@ -47,7 +47,7 @@ module Kaui
47
47
  @errors.add(:destroy, 'Destroying this object is not yet supported')
48
48
  false
49
49
  end
50
- # rubocop:enable Naming/PredicateMethod
50
+ # rubocop:enable Naming/PredicateMethod, ThreadSafety/ClassInstanceVariable
51
51
  end
52
52
 
53
53
  base_class.instance_eval do
@@ -25,12 +25,12 @@ module Kaui
25
25
 
26
26
  ALL_OBJECT_TYPES.each do |object_type|
27
27
  define_singleton_method "all_for_#{object_type.downcase}" do |options_for_klient|
28
- all('NONE', options_for_klient).delete_if { |tag_definition| !tag_definition.applicable_object_types.include?(object_type) }.sort
28
+ all('NONE', options_for_klient).delete_if { |tag_definition| tag_definition.applicable_object_types.exclude?(object_type) }.sort
29
29
  end
30
30
  end
31
31
 
32
32
  def system_tag?
33
- return false unless id.present?
33
+ return false if id.blank?
34
34
 
35
35
  last_group = id.split('-')[4]
36
36
 
@@ -7,7 +7,8 @@ module Kaui
7
7
  attribute :encrypted_api_secret, :encrypted, random_iv: false
8
8
  alias_attribute :api_secret, :encrypted_api_secret
9
9
 
10
- has_many :kaui_allowed_user_tenants, class_name: 'Kaui::AllowedUserTenant', foreign_key: 'kaui_tenant_id'
10
+ has_many :kaui_allowed_user_tenants, class_name: 'Kaui::AllowedUserTenant', foreign_key: 'kaui_tenant_id',
11
+ dependent: :destroy, inverse_of: :kaui_tenant
11
12
  has_many :kaui_allowed_users, through: :kaui_allowed_user_tenants, source: :kaui_allowed_user
12
13
  end
13
14
  end
@@ -6,7 +6,7 @@ module Kaui
6
6
 
7
7
  def self.build_from_raw_transaction(raw_transaction)
8
8
  result = Kaui::Transaction.new
9
- KillBillClient::Model::PaymentTransactionAttributes.instance_variable_get('@json_attributes').each do |attr|
9
+ KillBillClient::Model::PaymentTransactionAttributes.instance_variable_get(:@json_attributes).each do |attr|
10
10
  result.send("#{attr}=", raw_transaction.send(attr))
11
11
  end
12
12
  result
@@ -3,7 +3,7 @@
3
3
  module Dependencies
4
4
  module Kenui
5
5
  class EmailNotification
6
- ERROR_MESSAGE = I18n.translate('errors.messages.email_notification_plugin_not_available')
6
+ ERROR_MESSAGE = I18n.t('errors.messages.email_notification_plugin_not_available')
7
7
  class << self
8
8
  def email_notification_plugin_available?(options_for_klient)
9
9
  is_available = ::Kenui::EmailNotificationService.email_notification_plugin_available?(options_for_klient)
@@ -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>
@@ -59,13 +59,50 @@
59
59
  document.addEventListener("DOMContentLoaded", () => {
60
60
  const input = document.getElementById("search-box");
61
61
  const dropdown = document.getElementById("search-suggestions");
62
+ const validPrefixes = ['account:', 'invoice:', 'payment:', 'bundle:', 'custom field:', 'subscription:', 'tag:', 'transaction:', 'credit:', 'invoice payment:', 'tag definition:'];
63
+
64
+ // Get current prefix and search term from input value
65
+ function parseInput(value) {
66
+ const colonIndex = value.indexOf(':');
67
+ if (colonIndex === -1) {
68
+ return { prefix: '', searchTerm: value };
69
+ }
70
+ return {
71
+ prefix: value.substring(0, colonIndex + 1).toLowerCase(),
72
+ searchTerm: value.substring(colonIndex + 1)
73
+ };
74
+ }
75
+
76
+ // Ensure input always has a valid prefix
77
+ function ensurePrefix() {
78
+ const { prefix, searchTerm } = parseInput(input.value);
79
+ const hasValidPrefix = validPrefixes.some(p => prefix === p.toLowerCase());
80
+ if (!hasValidPrefix) {
81
+ input.value = 'account: ' + input.value.replace(/^[^:]*:?\s*/, '');
82
+ }
83
+ }
62
84
 
63
85
  input.addEventListener("input", () => {
64
86
  dropdown.classList.remove("d-none");
87
+ ensurePrefix();
65
88
  });
66
89
 
67
90
  input.addEventListener("focus", () => {
68
91
  dropdown.classList.remove("d-none");
92
+ // Add default prefix when focusing on empty input
93
+ if (input.value.trim() === '') {
94
+ input.value = 'account: ';
95
+ }
96
+ });
97
+
98
+ input.addEventListener("blur", () => {
99
+ // Clear back to empty if only prefix remains
100
+ const trimmed = input.value.trim();
101
+ if (validPrefixes.some(p => trimmed.toLowerCase() === p.trim().toLowerCase())) {
102
+ input.value = '';
103
+ } else {
104
+ ensurePrefix();
105
+ }
69
106
  });
70
107
 
71
108
  document.addEventListener("click", (e) => {
@@ -79,7 +116,8 @@
79
116
  element.addEventListener("click", (e) => {
80
117
  e.preventDefault();
81
118
  const term = element.getAttribute("data-term");
82
- input.value = term;
119
+ const { searchTerm } = parseInput(input.value);
120
+ input.value = term + searchTerm;
83
121
  input.focus();
84
122
  // Keep dropdown open so user can continue typing
85
123
  });
@@ -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
  };
@@ -34,12 +34,18 @@
34
34
  <%= select_tag :plan_name, options_for_select(@plans), :class => 'form-control' %>
35
35
  </div>
36
36
  </div>
37
- <div class="form-group d-flex pb-3 border-bottom mb-3">
37
+ <div class="form-group d-flex pb-3">
38
38
  <%= label_tag :price_override, 'Price Override', :class => 'col-sm-3 control-label' %>
39
39
  <div class="col-sm-9">
40
40
  <%= number_field_tag :price_override, nil, :step => :any, :min => 0, :class => 'form-control' %>
41
41
  </div>
42
42
  </div>
43
+ <div class="form-group d-flex pb-3 border-bottom mb-3">
44
+ <%= label_tag :quantity, 'Quantity', :class => 'col-sm-3 control-label' %>
45
+ <div class="col-sm-9">
46
+ <%= number_field_tag :quantity, 1, :min => 1, :step => 1, :class => 'form-control' %>
47
+ </div>
48
+ </div>
43
49
  <div class="form-group d-flex pb-3">
44
50
  <%= label_tag :type_change, 'Type', :class => 'col-sm-3 control-label' %>
45
51
  <div class="col-sm-offset-2 col-sm-9">