kaui 4.0.19 → 4.0.21

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: f667be5cb34e03c3d1b4da18ef422934d9794643606c998cf1ac84f2e6d99f60
4
- data.tar.gz: e202ccb38314d766896d3b72235de05c2818b74b985cb8b2717ff4850ab0b4ae
3
+ metadata.gz: bedd9b4f949e19f4818536fb8374a3adfeab4b6adbb87d6e6fd146ee2e19c82a
4
+ data.tar.gz: 658d3fb91736468eb634d4ac3f54c1710d30717293b5d3ff8e2633f08c71df8a
5
5
  SHA512:
6
- metadata.gz: 18c8742fd7498cab0431aa9758b7f7ebda0e225d7fba76c9dd8a1fa97bd52a8678812b3a6e0397e1d15fc8128e59f451ee455ac7ebdfe8cb13de888a86101501
7
- data.tar.gz: ee27c185159e1b564e0a0d16322178ddf3fbbce4c78929a0dfd3747c4c198084f1269f3e7d261047b0a2589b5f08d70be3ab07f8d727dfcc969e372929f7395a
6
+ metadata.gz: 3a79003af9a6324459d100f84fd14717e4a432e26c58e201472d7e35a5ee693a6943ecbd6c423c2dffe86cdfd258f23008711f934d254f2babb82fe3e5cbadb2
7
+ data.tar.gz: 6cf9a83025b46de823588ff1783b7dd0c336f8e3ecfdaaf7888af336af706a55f6b033a04a1f736690466f9b09bd36b0239ee8cfd8170754dcd342343f0039e9
@@ -17,7 +17,7 @@ module Kaui
17
17
  return
18
18
  end
19
19
  @search_fields = Kaui::Account::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
20
- @dropdown_default = default_columns(Kaui.account_search_columns.call[2], Kaui::Account::SENSIVITE_DATA_FIELDS)
20
+ @dropdown_default = default_columns(Kaui.account_search_columns.call[2], Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
21
21
  @visible_columns = @dropdown_default
22
22
 
23
23
  @ordering = params[:ordering] || (@search_query.blank? ? 'desc' : 'asc')
@@ -47,7 +47,7 @@ module Kaui
47
47
  Kaui.account_search_columns.call(account, view_context)[1]
48
48
  end
49
49
 
50
- paginate searcher, data_extractor, formatter, default_columns(Kaui.account_search_columns.call[2], Kaui::Account::SENSIVITE_DATA_FIELDS)
50
+ paginate searcher, data_extractor, formatter, default_columns(Kaui.account_search_columns.call[2], Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
51
51
  end
52
52
 
53
53
  def download
@@ -47,6 +47,12 @@ module Kaui
47
47
  @overdue_xml = nil
48
48
  end
49
49
 
50
+ fetch_invoice_template = promise do
51
+ Kaui::AdminTenant.get_invoice_template(false, options)
52
+ rescue StandardError
53
+ @invoice_template = nil
54
+ end
55
+
50
56
  fetch_tenant_plugin_config = promise { Kaui::AdminTenant.get_tenant_plugin_config(options) }
51
57
 
52
58
  @catalog_versions = []
@@ -64,6 +70,12 @@ module Kaui
64
70
  @overdue = flash[:overdue_deleted] ? nil : wait(fetch_overdue)
65
71
  @overdue_xml = flash[:overdue_deleted] ? nil : wait(fetch_overdue_xml)
66
72
  @overdue_config_exists = @overdue_xml.present? || (@overdue&.overdue_states.present? && !@overdue.has_states)
73
+ @invoice_template = begin
74
+ wait(fetch_invoice_template)
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
67
79
  @tenant_plugin_config = begin
68
80
  wait(fetch_tenant_plugin_config)
69
81
  rescue StandardError
@@ -367,6 +379,22 @@ module Kaui
367
379
  redirect_to admin_tenant_path(current_tenant.id, active_tab: 'OverdueShow'), notice: I18n.t('flashes.notices.overdue_deleted_successfully')
368
380
  end
369
381
 
382
+ def invoice_template
383
+ current_tenant = safely_find_tenant_by_id(params[:id])
384
+
385
+ options = tenant_options_for_client
386
+ options[:api_key] = current_tenant.api_key
387
+ options[:api_secret] = current_tenant.api_secret
388
+
389
+ template = Kaui::AdminTenant.get_invoice_template(false, options)
390
+
391
+ if template.present?
392
+ render body: template, content_type: 'text/html'
393
+ else
394
+ render plain: 'No invoice template found', status: :not_found
395
+ end
396
+ end
397
+
370
398
  def upload_invoice_template
371
399
  current_tenant = safely_find_tenant_by_id(params[:id])
372
400
 
@@ -380,7 +408,7 @@ module Kaui
380
408
 
381
409
  Kaui::AdminTenant.upload_invoice_template(invoice_template, is_manual_pay, true, options[:username], nil, comment, options)
382
410
 
383
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.invoice_template_uploaded_successfully')
411
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTemplate'), notice: I18n.t('flashes.notices.invoice_template_uploaded_successfully')
384
412
  end
385
413
 
386
414
  def upload_invoice_translation
@@ -394,9 +422,14 @@ module Kaui
394
422
  uploaded_invoice_translation = params.require(:invoice_translation)
395
423
  invoice_translation = uploaded_invoice_translation.read
396
424
 
425
+ if locale.blank?
426
+ flash[:error] = I18n.t('errors.messages.locale_required')
427
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTranslation') and return
428
+ end
429
+
397
430
  Kaui::AdminTenant.upload_invoice_translation(invoice_translation, locale, true, options[:username], nil, comment, options)
398
431
 
399
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.invoice_translation_uploaded_successfully')
432
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'InvoiceTranslation'), notice: I18n.t('flashes.notices.invoice_translation_uploaded_successfully')
400
433
  end
401
434
 
402
435
  def upload_catalog_translation
@@ -410,9 +443,14 @@ module Kaui
410
443
  uploaded_catalog_translation = params.require(:catalog_translation)
411
444
  catalog_translation = uploaded_catalog_translation.read
412
445
 
446
+ if locale.blank?
447
+ flash[:error] = I18n.t('errors.messages.locale_required')
448
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'CatalogTranslation') and return
449
+ end
450
+
413
451
  Kaui::AdminTenant.upload_catalog_translation(catalog_translation, locale, true, options[:username], nil, comment, options)
414
452
 
415
- redirect_to admin_tenant_path(current_tenant.id), notice: I18n.t('flashes.notices.catalog_translation_uploaded_successfully')
453
+ redirect_to admin_tenant_path(current_tenant.id, active_tab: 'CatalogTranslation'), notice: I18n.t('flashes.notices.catalog_translation_uploaded_successfully')
416
454
  end
417
455
 
418
456
  def upload_plugin_config
@@ -203,8 +203,8 @@ module Kaui
203
203
  render json: response, status: response_status
204
204
  end
205
205
 
206
- def default_columns(fields, sensivite_fields)
207
- fields.map { |field| { data: fields.index(field), visible: !(sensivite_fields.include? field) } }
206
+ def default_columns(fields, visible_fields)
207
+ fields.each_with_index.map { |field, index| { data: index, visible: visible_fields.include?(field) } }
208
208
  end
209
209
  end
210
210
  end
@@ -10,6 +10,8 @@ module Kaui
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] }
13
+ @dropdown_default = default_columns(Kaui.account_invoices_columns.call[3], Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS)
14
+ @visible_columns = @dropdown_default
13
15
 
14
16
  @max_nb_records = @search_query.blank? ? Kaui::Invoice.list_or_search(nil, 0, 0, options_for_klient).pagination_max_nb_records : 0
15
17
  end
@@ -11,6 +11,8 @@ module Kaui
11
11
  @offset = params[:offset] || 0
12
12
  @limit = params[:limit] || 50
13
13
  @search_fields = Kaui::Payment::ADVANCED_SEARCH_COLUMNS.map { |attr| [attr, attr.split('_').join(' ').capitalize] }
14
+ @dropdown_default = default_columns(Kaui.account_payments_columns.call[3], Kaui::Payment::DEFAULT_VISIBLE_COLUMNS)
15
+ @visible_columns = @dropdown_default
14
16
 
15
17
  @max_nb_records = @search_query.blank? ? Kaui::Payment.list_or_search(nil, 0, 0, options_for_klient).pagination_max_nb_records : 0
16
18
  end
@@ -353,7 +353,7 @@ module Kaui
353
353
  return nil if raw.blank?
354
354
 
355
355
  entries = raw.respond_to?(:values) ? raw.values : Array(raw)
356
- phase_meta = (plan_details.phases || []).index_by(&:type)
356
+ phase_meta = plan_details.respond_to?(:phases) ? (plan_details.phases || []).index_by(&:type) : {}
357
357
 
358
358
  overrides = entries.filter_map do |entry|
359
359
  entry = entry.to_unsafe_h if entry.respond_to?(:to_unsafe_h)
@@ -384,18 +384,25 @@ module Kaui
384
384
 
385
385
  def build_plan_phases_map(plans_details)
386
386
  (plans_details || []).to_h do |pd|
387
- phases = (pd.phases || []).map do |ph|
388
- fixed = phase_uses_fixed_price?(ph)
389
- prices = ph.prices || []
390
- price_label = if fixed
391
- '$0.00'
392
- elsif prices.any?
393
- format('$%.2f', prices.first.value.to_f)
394
- else
395
- ''
396
- end
397
- { type: ph.type, fixed: fixed, priceLabel: price_label }
398
- end
387
+ # PlanDetail objects returned by available_base_plans/available_addons only expose the
388
+ # final phase (product/plan/priceList/finalPhaseBillingPeriod/finalPhaseRecurringPrice)
389
+ # and have no phases method, unlike full catalog Plan objects. Skip phase overrides for those.
390
+ phases = if pd.respond_to?(:phases)
391
+ (pd.phases || []).map do |ph|
392
+ fixed = phase_uses_fixed_price?(ph)
393
+ prices = ph.prices || []
394
+ price_label = if fixed
395
+ '$0.00'
396
+ elsif prices.any?
397
+ format('$%.2f', prices.first.value.to_f)
398
+ else
399
+ ''
400
+ end
401
+ { type: ph.type, fixed: fixed, priceLabel: price_label }
402
+ end
403
+ else
404
+ []
405
+ end
399
406
  [pd.plan, phases]
400
407
  end
401
408
  end
@@ -4,7 +4,9 @@ module Kaui
4
4
  class Account < KillBillClient::Model::Account
5
5
  attr_accessor :phone, :bill_cycle_day_local
6
6
 
7
- SENSIVITE_DATA_FIELDS = %w[name email].freeze
7
+ # Columns shown by default on the Accounts list screen (demo-friendly defaults); the rest remain
8
+ # available but hidden until the user opts in via "Edit Columns".
9
+ DEFAULT_VISIBLE_COLUMNS = %w[name account_id external_key currency time_zone locale account_balance account_cba].freeze
8
10
  REMAPPING_FIELDS = {
9
11
  'is_payment_delegated_to_parent' => 'pay_via_parent',
10
12
  'account_balance' => 'balance',
@@ -15,6 +15,10 @@ module Kaui
15
15
  KillBillClient::Model::Invoice.upload_invoice_template(invoice_template, is_manual_pay, delete_if_exists, user, reason, comment, options)
16
16
  end
17
17
 
18
+ def get_invoice_template(is_manual_pay, options = {})
19
+ KillBillClient::Model::Invoice.get_invoice_template(is_manual_pay, nil, options)
20
+ end
21
+
18
22
  def upload_invoice_translation(invoice_translation, locale, delete_if_exists, user = nil, reason = nil, comment = nil, options = {})
19
23
  KillBillClient::Model::Invoice.upload_invoice_translation(invoice_translation, locale, delete_if_exists, user, reason, comment, options)
20
24
  end
@@ -3,6 +3,9 @@
3
3
  module Kaui
4
4
  class Invoice < KillBillClient::Model::Invoice
5
5
  TABLE_IGNORE_COLUMNS = %w[amount balance credit_adj refund_adj items is_parent_invoice parent_invoice_id parent_account_id audit_logs bundle_keys].freeze
6
+ # Columns shown by default on the Invoices list screen (demo-friendly defaults); the rest remain
7
+ # available but hidden until the user opts in via "Edit Columns".
8
+ DEFAULT_VISIBLE_COLUMNS = %w[invoice_number invoice_id status invoice_date target_date currency account_id].freeze
6
9
  ADVANCED_SEARCH_COLUMNS = %w[id account_id invoice_date target_date currency status balance].freeze
7
10
  ADVANCED_SEARCH_NAME_CHANGES = [%w[ac_id account_id]].freeze
8
11
 
@@ -7,6 +7,9 @@ module Kaui
7
7
  attr_accessor :payment_date, :target_invoice_id
8
8
 
9
9
  TRANSACTION_STATUSES = %w[SUCCESS PENDING PAYMENT_FAILURE PLUGIN_FAILURE UNKNOWN].freeze
10
+ # Columns shown by default on the Payments list screen (demo-friendly defaults); the rest remain
11
+ # available but hidden until the user opts in via "Edit Columns".
12
+ DEFAULT_VISIBLE_COLUMNS = %w[payment_date payment_number status account_id currency purchased_amount refunded_amount credited_amount].freeze
10
13
  REMAPPING_FIELDS = {
11
14
  'auth_amount' => 'auth',
12
15
  'captured_amount' => 'capture',
@@ -378,7 +378,7 @@ $(document).ready(function() {
378
378
  updateDropdownOrder();
379
379
 
380
380
  function updateDropdownOrder() {
381
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
381
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
382
382
  if (state === null) {
383
383
  // On initial load, ensure the existing checkboxes work
384
384
  setupColumnToggleEvents();
@@ -393,7 +393,7 @@ $(document).ready(function() {
393
393
 
394
394
  if (columnOrder !== undefined) {
395
395
  $list.empty();
396
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
396
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
397
397
  if (state !== null) {
398
398
  var colsOrder = state.ColReorder;
399
399
  }
@@ -319,7 +319,7 @@ $(document).ready(function() {
319
319
  updateDropdownOrder();
320
320
 
321
321
  function updateDropdownOrder() {
322
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
322
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
323
323
  if (state === null) {
324
324
  return;
325
325
  }
@@ -332,7 +332,7 @@ $(document).ready(function() {
332
332
 
333
333
  if (columnOrder !== undefined) {
334
334
  $list.empty();
335
- var state = JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
335
+ var state = JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
336
336
  if (state !== null) {
337
337
  var colsOrder = state.ColReorder;
338
338
  }
@@ -79,11 +79,12 @@ $(document).ready(function() {
79
79
  "enable": false
80
80
  },
81
81
  "stateSave": true,
82
+ "stateDuration": -1,
82
83
  "stateSaveCallback": function(settings, data) {
83
- localStorage.setItem('DataTables_accounts-table_' + window.location.pathname, JSON.stringify(data));
84
+ localStorage.setItem('DataTables_accounts-table-v2_' + window.location.pathname, JSON.stringify(data));
84
85
  },
85
86
  "stateLoadCallback": function(settings) {
86
- return JSON.parse(localStorage.getItem('DataTables_accounts-table_' + window.location.pathname));
87
+ return JSON.parse(localStorage.getItem('DataTables_accounts-table-v2_' + window.location.pathname));
87
88
  },
88
89
  "scrollX": true,
89
90
  "dom": "<'row'r>t<'row'<'col-md-6'i><'col-md-6'p>>",
@@ -21,7 +21,7 @@
21
21
  <div class="form-group d-flex pb-3 border-bottom mb-3">
22
22
  <%= label_tag :translation_locale, 'Locale', :class => 'col-sm-2 control-label' %>
23
23
  <div class="col-sm-10">
24
- <%= text_field_tag :translation_locale, nil, :class => 'form-control' %>
24
+ <%= text_field_tag :translation_locale, nil, :class => 'form-control', :required => true, :placeholder => 'e.g. en_US' %>
25
25
  </div>
26
26
  </div>
27
27
  <div class="d-flex justify-content-end">
@@ -1,5 +1,28 @@
1
1
  <% if can? :config_upload, Kaui::AdminTenant %>
2
2
  <div class="tab-pane fade invoice-template form-invoice-template" id="InvoiceTemplate">
3
+ <% if @invoice_template.present? %>
4
+ <div class="invoice-template-display mb-4">
5
+ <h5>Current Invoice Template</h5>
6
+ <div class="card">
7
+ <div class="card-body p-0">
8
+ <iframe src="<%= admin_tenant_invoice_template_path(@tenant.id) %>" style="width: 100%; height: 500px; border: none;" title="Invoice Template Preview"></iframe>
9
+ </div>
10
+ </div>
11
+ <div class="mt-2">
12
+ <%= render "kaui/components/button/button", {
13
+ label: 'View Source',
14
+ variant: "outline-secondary d-inline-flex align-items-center gap-1",
15
+ type: "button",
16
+ html_class: "kaui-dropdown custom-hover",
17
+ html_options: {
18
+ onclick: '$(this).toggleClass("active"); $(this).next("pre").toggle(); return false;'
19
+ }
20
+ } %>
21
+ <pre class="mb-0 mt-2 p-2 bg-light" style="display: none; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-family: monospace; font-size: 12px;"><%= html_escape(@invoice_template) %></pre>
22
+ </div>
23
+ </div>
24
+ <% end %>
25
+
3
26
  <%= form_tag({:action => :upload_invoice_template}, :method => 'post', :multipart => true, :class => 'form-horizontal') do %>
4
27
  <%= hidden_field_tag(:id, @tenant.id) %>
5
28
 
@@ -19,8 +42,6 @@
19
42
  %>
20
43
  </div>
21
44
 
22
-
23
-
24
45
  <div class="form-group pb-3 border-bottom mb-3">
25
46
  <div class="col-sm-10">
26
47
  <div class="checkbox">
@@ -21,7 +21,7 @@
21
21
  <div class="form-group d-flex pb-3 border-bottom mb-3">
22
22
  <%= label_tag :translation_locale, 'Locale', :class => 'col-sm-2 control-label' %>
23
23
  <div class="col-sm-10">
24
- <%= text_field_tag :translation_locale, nil, :class => 'form-control' %>
24
+ <%= text_field_tag :translation_locale, nil, :class => 'form-control', :required => true, :placeholder => 'e.g. en_US' %>
25
25
  </div>
26
26
  </div>
27
27
  <div class="d-flex justify-content-end">
@@ -52,7 +52,7 @@
52
52
  <% Kaui.account_invoices_columns.call[0].each_with_index do |title, index| %>
53
53
  <li class="list-group-item-manual" data-id="<%= index %>">
54
54
  <label class="label-group-item-manual">
55
- <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" checked>
55
+ <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" <%= 'checked' if @dropdown_default[index][:visible] %>>
56
56
  <%= title %>
57
57
  <span class="icon-drag" aria-hidden="true"></span>
58
58
  </label>
@@ -337,7 +337,7 @@ $(document).ready(function() {
337
337
  updateDropdownOrder();
338
338
 
339
339
  function loadState() {
340
- var state = JSON.parse(localStorage.getItem('DataTables_invoices-table'));
340
+ var state = JSON.parse(localStorage.getItem('DataTables_invoices-table-v2'));
341
341
  return state || { columns: [], columnOrder: [] };
342
342
  }
343
343
 
@@ -50,7 +50,7 @@ $(document).ready(function() {
50
50
  }
51
51
  });
52
52
 
53
- var stateKey = 'DataTables_invoices-table';
53
+ var stateKey = 'DataTables_invoices-table-v2';
54
54
  var state = JSON.parse(localStorage.getItem(stateKey));
55
55
  if (state) {
56
56
  state.start = <%= @offset %>;
@@ -63,6 +63,7 @@ $(document).ready(function() {
63
63
  "enable": false
64
64
  },
65
65
  "stateSave": true,
66
+ "stateDuration": -1,
66
67
  "stateSaveCallback": function(settings, data) {
67
68
  localStorage.setItem(stateKey, JSON.stringify(data));
68
69
  },
@@ -99,7 +100,8 @@ $(document).ready(function() {
99
100
  });
100
101
  return reorderedData;
101
102
  }
102
- }
103
+ },
104
+ "columns": <%= raw @visible_columns.to_json %>
103
105
  });
104
106
 
105
107
  // If the page loaded with advanced search params in the URL (e.g. restored from
@@ -52,7 +52,7 @@
52
52
  <% Kaui.account_payments_columns.call()[0].each_with_index do |title, index| %>
53
53
  <li class="list-group-item-manual" data-id="<%= index %>">
54
54
  <label class="label-group-item-manual">
55
- <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" checked>
55
+ <input type="checkbox" class="column-toggle" draggable="true" data-column="<%= index %>" <%= 'checked' if @dropdown_default[index][:visible] %>>
56
56
  <%= title %>
57
57
  <span class="icon-drag" aria-hidden="true"></span>
58
58
  </label>
@@ -338,7 +338,7 @@ $(document).ready(function() {
338
338
  updateDropdownOrder();
339
339
 
340
340
  function loadState() {
341
- var state = JSON.parse(localStorage.getItem('DataTables_payments-table'));
341
+ var state = JSON.parse(localStorage.getItem('DataTables_payments-table-v2'));
342
342
  return state || { columns: [], columnOrder: [] };
343
343
  }
344
344
 
@@ -41,7 +41,7 @@
41
41
 
42
42
  <%= javascript_tag do %>
43
43
  $(document).ready(function() {
44
- var stateKey = 'DataTables_payments-table';
44
+ var stateKey = 'DataTables_payments-table-v2';
45
45
  var state = JSON.parse(localStorage.getItem(stateKey));
46
46
  if (state) {
47
47
  state.start = <%= @offset %>;
@@ -54,11 +54,12 @@ $(document).ready(function() {
54
54
  "enable": false
55
55
  },
56
56
  "stateSave": true,
57
+ "stateDuration": -1,
57
58
  "stateSaveCallback": function(settings, data) {
58
- localStorage.setItem('DataTables_payments-table', JSON.stringify(data));
59
+ localStorage.setItem(stateKey, JSON.stringify(data));
59
60
  },
60
61
  "stateLoadCallback": function(settings) {
61
- return JSON.parse(localStorage.getItem('DataTables_payments-table'));
62
+ return JSON.parse(localStorage.getItem(stateKey));
62
63
  },
63
64
  "scrollX": true,
64
65
  <% if @account.account_id.blank? %>
@@ -115,6 +116,7 @@ $(document).ready(function() {
115
116
  "processing": true,
116
117
  "serverSide": true,
117
118
  "search": {"search": "<%= @search_query %>"},
119
+ "columns": <%= raw @visible_columns.to_json %>
118
120
  });
119
121
 
120
122
  // If the page loaded with advanced search params in the URL (e.g. restored from
@@ -27,7 +27,7 @@
27
27
  <div style="width: 40px;"></div>
28
28
  </div>
29
29
  <div id="phase_overrides_rows"></div>
30
- <button type="button" id="add_phase_override" class="btn w-100 mt-2 d-flex align-items-center justify-content-center gap-1"
30
+ <button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
31
31
  style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
32
32
  <span style="font-size: 16px; line-height: 1;">+</span> Add another phase
33
33
  </button>
@@ -161,7 +161,13 @@ function initPhaseOverrides() {
161
161
  var $planSelect = $('select[name="plan_name"]');
162
162
 
163
163
  function currentPhases() {
164
- return planPhases[$planSelect.val()] || [];
164
+ var phases = planPhases[$planSelect.val()] || [];
165
+ var seen = {};
166
+ return phases.filter(function(p) {
167
+ if (!p || !p.type || seen[p.type]) return false;
168
+ seen[p.type] = true;
169
+ return true;
170
+ });
165
171
  }
166
172
 
167
173
  function phaseLabel(p) {
@@ -177,9 +183,13 @@ function initPhaseOverrides() {
177
183
 
178
184
  function refreshAddBtn() {
179
185
  var phases = currentPhases();
180
- var used = selectedTypes();
186
+ var used = selectedTypes().filter(function(type) { return type; });
181
187
  var hasMore = phases.length > 0 && used.length < phases.length;
182
- $addBtn.toggle(hasMore);
188
+ if (hasMore) {
189
+ $addBtn.css('display', 'flex');
190
+ } else {
191
+ $addBtn.hide();
192
+ }
183
193
  $headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
184
194
  }
185
195
 
@@ -205,7 +215,7 @@ function initPhaseOverrides() {
205
215
  function addRow(preferredType) {
206
216
  var phases = currentPhases();
207
217
  if (phases.length === 0) return;
208
- var used = selectedTypes();
218
+ var used = selectedTypes().filter(function(type) { return type; });
209
219
  var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
210
220
  ? preferredType
211
221
  : (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
@@ -37,11 +37,11 @@
37
37
  <div class="form-group d-flex pb-3">
38
38
  <div class="col-sm-3 control-label" style="padding-top: 2px;">
39
39
  <div id="toggle_phase_overrides" style="cursor: pointer; user-select: none;">
40
- <span id="toggle_phase_overrides_caret">&#9662;</span>
41
- <strong>Phase Overrides</strong>
40
+ <span id="toggle_phase_overrides_caret">&#9656;</span>
41
+ <strong>Price Override</strong>
42
42
  </div>
43
43
  </div>
44
- <div class="col-sm-9" id="phase_overrides_section">
44
+ <div class="col-sm-9" id="phase_overrides_section" style="display: none;">
45
45
  <p class="text-muted mb-2" style="font-size: 13px;">Override prices for specific phases. All other phases will use catalog pricing.</p>
46
46
  <div id="phase_overrides_headers" class="d-flex mb-1 gap-2" style="display: none;">
47
47
  <div style="flex: 1; padding-left: 2px;"><small><strong>Phase</strong></small></div>
@@ -55,7 +55,7 @@
55
55
  <div style="width: 40px;"></div>
56
56
  </div>
57
57
  <div id="phase_overrides_rows"></div>
58
- <button type="button" id="add_phase_override" class="btn w-100 mt-2 d-flex align-items-center justify-content-center gap-1"
58
+ <button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
59
59
  style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
60
60
  <span style="font-size: 16px; line-height: 1;">+</span> Add another phase
61
61
  </button>
@@ -178,7 +178,13 @@ function initPhaseOverrides() {
178
178
  var $planSelect = $('select[name="plan_name"]');
179
179
 
180
180
  function currentPhases() {
181
- return planPhases[$planSelect.val()] || [];
181
+ var phases = planPhases[$planSelect.val()] || [];
182
+ var seen = {};
183
+ return phases.filter(function(p) {
184
+ if (!p || !p.type || seen[p.type]) return false;
185
+ seen[p.type] = true;
186
+ return true;
187
+ });
182
188
  }
183
189
 
184
190
  function phaseLabel(p) {
@@ -194,9 +200,13 @@ function initPhaseOverrides() {
194
200
 
195
201
  function refreshAddBtn() {
196
202
  var phases = currentPhases();
197
- var used = selectedTypes();
203
+ var used = selectedTypes().filter(function(type) { return type; });
198
204
  var hasMore = phases.length > 0 && used.length < phases.length;
199
- $addBtn.toggle(hasMore);
205
+ if (hasMore) {
206
+ $addBtn.css('display', 'flex');
207
+ } else {
208
+ $addBtn.hide();
209
+ }
200
210
  $headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
201
211
  }
202
212
 
@@ -222,7 +232,7 @@ function initPhaseOverrides() {
222
232
  function addRow(preferredType) {
223
233
  var phases = currentPhases();
224
234
  if (phases.length === 0) return;
225
- var used = selectedTypes();
235
+ var used = selectedTypes().filter(function(type) { return type; });
226
236
  var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
227
237
  ? preferredType
228
238
  : (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
@@ -27,7 +27,7 @@
27
27
  <% if @unit_types.length == 1 %>
28
28
  <%= text_field_tag :unit_type, @unit_types.first, readonly: true, required: true, class: 'form-control bg-light' %>
29
29
  <% elsif @unit_types.length > 1 %>
30
- <%= select_tag :unit_type, options_for_select(@unit_types.map { |u| [u, u] }, @unit_type.presence || @unit_types.first), required: true, class: 'form-select' %>
30
+ <%= select_tag :unit_type, options_for_select(@unit_types.map { |u| [u, u] }, @unit_type.presence || @unit_types.first), required: true, class: 'form-control', title: (@unit_type.presence || @unit_types.first) %>
31
31
  <% else %>
32
32
  <%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: 'e.g. api-calls' %>
33
33
  <% end %>
@@ -44,11 +44,40 @@
44
44
  <div class="form-group d-flex pb-3">
45
45
  <%= label_tag :record_date, 'Date/Time of Usage', class: 'col-sm-3 control-label' %>
46
46
  <div class="col-sm-9">
47
- <%= datetime_field_tag :record_date,
48
- @record_date.presence || Time.now.utc.strftime('%Y-%m-%dT%H:%M'),
49
- required: true,
50
- class: 'form-control' %>
51
- <small class="form-text text-muted">Date and time treated as UTC</small>
47
+ <%
48
+ record_date_value = @record_date.to_s.strip
49
+ record_date_date_value = Time.now.utc.strftime('%Y-%m-%d')
50
+ record_date_time_value = '00:00'
51
+
52
+ begin
53
+ if record_date_value.match?(/^\d{4}-\d{2}-\d{2}$/)
54
+ record_date_date_value = record_date_value
55
+ elsif record_date_value.match?(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)
56
+ record_date_date_value = record_date_value[0, 10]
57
+ record_date_time_value = record_date_value[11, 5]
58
+ elsif !record_date_value.empty?
59
+ parsed_record_date = Time.iso8601(record_date_value).utc
60
+ record_date_date_value = parsed_record_date.strftime('%Y-%m-%d')
61
+ record_date_time_value = parsed_record_date.strftime('%H:%M')
62
+ end
63
+ rescue ArgumentError
64
+ record_date_date_value = Time.now.utc.strftime('%Y-%m-%d')
65
+ record_date_time_value = '00:00'
66
+ end
67
+ %>
68
+
69
+ <div class="d-flex gap-2 align-items-center">
70
+ <%= date_field_tag :record_date_date,
71
+ record_date_date_value,
72
+ required: true,
73
+ class: 'form-control' %>
74
+ <%= time_field_tag :record_date_time,
75
+ record_date_time_value,
76
+ step: 60,
77
+ class: 'form-control' %>
78
+ </div>
79
+ <%= hidden_field_tag :record_date, "#{record_date_date_value}T#{record_date_time_value}" %>
80
+ <small class="form-text text-muted">Pick a date. Time is optional and defaults to 00:00 (UTC) if unchanged.</small>
52
81
  </div>
53
82
  </div>
54
83
 
@@ -86,11 +115,19 @@
86
115
  document.addEventListener('DOMContentLoaded', function () {
87
116
  var form = document.getElementById('record_usage_form');
88
117
  if (!form) { return; }
118
+ var unitTypeSelect = document.getElementById('unit_type');
119
+ if (unitTypeSelect && unitTypeSelect.tagName === 'SELECT') {
120
+ unitTypeSelect.title = unitTypeSelect.value || '';
121
+ unitTypeSelect.addEventListener('change', function () {
122
+ unitTypeSelect.title = unitTypeSelect.value || '';
123
+ });
124
+ }
125
+
89
126
  form.addEventListener('submit', function (e) {
90
127
  var errors = [];
91
128
 
92
- var unitTypeEl = document.getElementById('unit_type');
93
- var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
129
+ var unitTypeEl = document.getElementById('unit_type');
130
+ var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
94
131
  if (unitType.length === 0) {
95
132
  errors.push('Unit type is required.');
96
133
  }
@@ -101,6 +138,31 @@
101
138
  errors.push('Amount must be a positive integer.');
102
139
  }
103
140
 
141
+ var dateEl = document.getElementById('record_date_date');
142
+ var timeEl = document.getElementById('record_date_time');
143
+ var hiddenDateEl = document.getElementById('record_date');
144
+
145
+ var datePart = dateEl ? (dateEl.value || '').trim() : '';
146
+ var timePart = timeEl ? (timeEl.value || '').trim() : '';
147
+ if (timePart.length === 0) {
148
+ timePart = '00:00';
149
+ }
150
+
151
+ var dateRegex = /^\d{4}-\d{2}-\d{2}$/;
152
+ var timeRegex = /^\d{2}:\d{2}$/;
153
+ if (datePart.length === 0) {
154
+ errors.push('Date/time of usage is required.');
155
+ } else if (!dateRegex.test(datePart)) {
156
+ errors.push('Date of usage must be in YYYY-MM-DD format.');
157
+ } else if (!timeRegex.test(timePart)) {
158
+ errors.push('Time must be in HH:MM format.');
159
+ } else {
160
+ var composedDateTime = datePart + 'T' + timePart;
161
+ if (hiddenDateEl) {
162
+ hiddenDateEl.value = composedDateTime;
163
+ }
164
+ }
165
+
104
166
  if (errors.length > 0) {
105
167
  e.preventDefault();
106
168
  alert(errors.join('\n'));
@@ -98,6 +98,7 @@ en:
98
98
  unable_to_connect_database: "Unable to connect to the database."
99
99
  unable_to_connect_killbill: "Unable to connect to the Kill Bill server."
100
100
  error_communicating_killbill: "Error while communicating with the Kill Bill server."
101
+ locale_required: "Please specify a Locale (e.g. en_US) for the translation file."
101
102
  views:
102
103
  subscriptions:
103
104
  requested_date_for_billing_notice: <ul>
data/config/routes.rb CHANGED
@@ -202,6 +202,7 @@ Kaui::Engine.routes.draw do
202
202
  post '/modify_overdue_config' => 'admin_tenants#modify_overdue_config', :as => 'admin_tenant_modify_overdue_config'
203
203
  post '/upload_overdue_config' => 'admin_tenants#upload_overdue_config', :as => 'admin_tenant_upload_overdue_config'
204
204
  post '/upload_invoice_template' => 'admin_tenants#upload_invoice_template', :as => 'admin_tenant_upload_invoice_template'
205
+ get '/:id/invoice_template' => 'admin_tenants#invoice_template', :as => 'admin_tenant_invoice_template'
205
206
  post '/upload_invoice_translation' => 'admin_tenants#upload_invoice_translation', :as => 'admin_tenant_upload_invoice_translation'
206
207
  post '/upload_catalog_translation' => 'admin_tenants#upload_catalog_translation', :as => 'admin_tenant_upload_catalog_translation'
207
208
  post '/upload_plugin_config' => 'admin_tenants#upload_plugin_config', :as => 'admin_tenant_upload_plugin_config'
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.19'
4
+ VERSION = '4.0.21'
5
5
  end
data/lib/kaui.rb CHANGED
@@ -66,6 +66,8 @@ module Kaui
66
66
  # Add additional fields if needed
67
67
  fields = original_fields.dup
68
68
  fields -= %w[audit_logs first_name_length]
69
+ # Demo-friendly default column order (see Kaui::Account::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
70
+ fields = Kaui::Account::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Account::DEFAULT_VISIBLE_COLUMNS)
69
71
  headers = fields.dup
70
72
  Kaui::Account::REMAPPING_FIELDS.each do |k, v|
71
73
  headers[fields.index(k)] = v
@@ -115,8 +117,8 @@ module Kaui
115
117
  fields = KillBillClient::Model::InvoiceAttributes.instance_variable_get(:@json_attributes)
116
118
  # Change the order if needed
117
119
  fields -= Kaui::Invoice::TABLE_IGNORE_COLUMNS
118
- fields.delete('invoice_id')
119
- fields.unshift('invoice_id')
120
+ # Demo-friendly default column order (see Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
121
+ fields = Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Invoice::DEFAULT_VISIBLE_COLUMNS)
120
122
 
121
123
  headers = fields.map { |attr| attr.split('_').join(' ').capitalize }
122
124
  # Add additional headers if needed
@@ -146,16 +148,16 @@ module Kaui
146
148
 
147
149
  raw_data = fields.map { |attr| invoice&.send(attr.downcase) }
148
150
 
149
- [headers, values, raw_data]
151
+ [headers, values, raw_data, fields]
150
152
  end
151
153
 
152
154
  self.account_payments_columns = lambda do |account = nil, payment = nil, view_context = nil|
153
155
  fields = KillBillClient::Model::PaymentAttributes.instance_variable_get(:@json_attributes)
154
156
  # Change the order if needed
155
- fields = %w[payment_date] + fields
156
- fields -= %w[payment_number transactions audit_logs]
157
- fields.unshift('status')
158
- fields.unshift('payment_number')
157
+ fields = %w[payment_date status] + fields
158
+ fields -= %w[transactions audit_logs]
159
+ # Demo-friendly default column order (see Kaui::Payment::DEFAULT_VISIBLE_COLUMNS for the ones shown by default)
160
+ fields = Kaui::Payment::DEFAULT_VISIBLE_COLUMNS + (fields - Kaui::Payment::DEFAULT_VISIBLE_COLUMNS)
159
161
 
160
162
  headers = fields.dup
161
163
  Kaui::Payment::REMAPPING_FIELDS.each do |k, v|
@@ -163,7 +165,7 @@ module Kaui
163
165
  end
164
166
  headers.map! { |attr| attr.split('_').join(' ').capitalize }
165
167
 
166
- return [headers, []] if payment.nil?
168
+ return [headers, [], [], fields] if payment.nil?
167
169
 
168
170
  values = fields.map do |attr|
169
171
  case attr
@@ -190,7 +192,7 @@ module Kaui
190
192
  end
191
193
 
192
194
  # Add additional values if needed
193
- [headers, values, raw_data]
195
+ [headers, values, raw_data, fields]
194
196
  end
195
197
 
196
198
  self.account_audit_logs_columns = lambda do
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.19
4
+ version: 4.0.21
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-07-01 00:00:00.000000000 Z
11
+ date: 2026-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack