diff --git a/app/assets/stylesheets/kaui/auth_pages.css b/app/assets/stylesheets/kaui/auth_pages.css index 3fb6d9552..69a6c091f 100644 --- a/app/assets/stylesheets/kaui/auth_pages.css +++ b/app/assets/stylesheets/kaui/auth_pages.css @@ -2,6 +2,15 @@ min-height: 100vh; width: 100%; background-color: #fff; + overflow: hidden; +} + +.auth-container .centered-absolute { + position: relative; + top: 0; + left: 0; + transform: none; + margin: 1.5rem auto 0; } .auth-form-container { diff --git a/app/assets/stylesheets/kaui/kaui.css b/app/assets/stylesheets/kaui/kaui.css index 2d755645f..70e884ca7 100644 --- a/app/assets/stylesheets/kaui/kaui.css +++ b/app/assets/stylesheets/kaui/kaui.css @@ -16,209 +16,3 @@ image-url("glyphicons-halflings-regular.ttf") format("truetype"), image-url("glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg"); } - -.tag-bar { - border-bottom: 0.0625rem solid #ddd; - margin-bottom: 0; - position: relative; -} - -.tag-bar .tag-select { - display: inline-block; - vertical-align: middle; -} - -@media (max-width: 28.125rem) { - .tag-bar .tag-select { - width: 35%; - } -} - -.tag-bar .tag-select span { - padding: 0.3125rem 1.25rem; - text-align: center; - display: block; - border-right: 0.0625rem solid #ddd; -} - -@media (max-width: 48rem) { - .tag-bar .tag-select span { - border: 0; - } -} - -.tag-bar .tag-select span i { - margin: 0 0.3125rem; - color: #515151; - font-size: 1rem; -} - -.tag-bar .tag-select span i.fa-caret-down { - color: #999999; - font-size: 0.75rem; -} - -.tag-bar .tag-select span i.fa-tag { - font-size: 1.125rem; -} - -.tag-bar .tag-select span i.fa-money, -.tag-bar .tag-select span i.fa-credit-card { - font-size: 1rem; -} - -.tag-bar .tag-select:hover .tag-select-box { - max-height: 31.25rem; - padding: 1.25rem 1.5625rem; - border: 0.0625rem solid #ddd; - box-shadow: 0.0625rem 0.1875rem 0.125rem rgba(0, 0, 0, 0.11); -} - -.tag-bar .tag-select .tag-select-box { - position: absolute; - top: 100%; - left: -0.0625rem; - max-height: 0; - background-color: #fff; - z-index: 100; - padding: 0 0.9375rem; - font-size: 0.75rem; - color: #999999; - overflow: auto; - width: auto; - border-bottom: 0; - display: inline-block; -} - -@media (max-width: 48rem) { - .tag-bar .tag-select .tag-select-box { - border: 0; - } -} - -.tag-bar .tag-select .tag-select-box strong { - display: block; - font-weight: 400; - margin: 0 0 0.625rem; -} - -.tag-bar .tag-select .tag-select-box form { - display: inline-block; - width: 13.125rem; -} - -.tag-bar .tag-select .tag-select-box input[type="checkbox"] { - display: inline-block; - width: 9%; - margin: 0 1% 0 0; - line-height: 1.25rem; - vertical-align: middle; -} - -.tag-bar .tag-select .tag-select-box label { - font-size: 0.75rem; - display: inline-block; - width: 85%; - line-height: 1.25rem; - vertical-align: middle; - margin: 0; - font-weight: 400; -} - -.tag-bar .tag-select .tag-select-box textarea { - resize: none; - height: 5rem; - margin: 0 0 1.25rem; - width: 100%; -} - -.tag-bar .tag-select .tag-select-box a { - display: block; - font-size: 0.75rem; - line-height: 1.25rem; -} - -.tag-bar .tag-selected { - display: inline-block; - padding: 0.125rem 0.3125rem; - vertical-align: middle; -} - -@media (max-width: 48rem) { - .tag-bar .tag-selected { - width: 64%; - padding: 0.125rem; - overflow: hidden; - border-left: 0.0625rem solid #ddd; - } -} - -.tag-bar .tag-selected span { - display: inline-block; - font-size: 0.75rem; - color: #777777; - background: #e5e5e5; - padding: 0.1875rem 1.25rem; - margin: 0 0.3125rem; - transition: all 0.5s; - cursor: pointer; -} - -.tag-bar .tag-selected span:hover { - background-color: #cce9eb; -} - -@media (max-width: 48rem) { - .tag-bar .tag-selected span { - font-size: 0.625rem; - padding: 0.1875rem 0.3125rem; - display: block; - margin: 0 0 0.125rem; - } -} - -.tag-bar-breathe { - margin-bottom: 1.875rem; -} - -.tag-bar-no-border { - border-bottom: 0; -} - -.tag-bar-no-border .tag-select span { - border-right: 0; -} - -#main-menu .tag-bar .tag-select { - position: relative; -} - -#main-menu .tag-bar .tag-select-box { - word-break: keep-all; - white-space: nowrap; -} - -.inline-row-tag-bar { - padding-left: 0.625rem !important; -} - -.inline-row-tag-bar .tag-bar .tag-select span { - padding: 0.3125rem; - border-right: none; -} - -.inline-row-tag-bar .tag-bar .tag-select span i { - margin: 0 0.1875rem; - font-size: 0.75rem; -} - -.inline-row-tag-bar .tag-bar .tag-select span i.fa-tag { - font-size: 0.75rem; -} - -.inline-row-tag-bar .tag-bar .tag-select-box label { - white-space: nowrap; -} - - -/*# sourceMappingURL=kaui2.css.map */ diff --git a/app/assets/stylesheets/kaui/tags.css b/app/assets/stylesheets/kaui/tags.css index 638b064ef..af77aaea5 100644 --- a/app/assets/stylesheets/kaui/tags.css +++ b/app/assets/stylesheets/kaui/tags.css @@ -15,7 +15,7 @@ } } -.tag-bar .tag-select span { +.tag-bar .tag-select > span { padding: 0.3125rem 1.25rem; text-align: center; display: block; @@ -23,7 +23,7 @@ } @media (max-width: 48rem) { - .tag-bar .tag-select span { + .tag-bar .tag-select > span { border: 0; } } @@ -67,9 +67,8 @@ font-size: 0.75rem; color: #999999; overflow: hidden; - min-width: 15rem; - width: max-content; - max-width: min(25rem, 95vw); + width: 17rem; + max-width: min(17rem, 95vw); border-bottom: 0; display: inline-block; } @@ -107,6 +106,7 @@ vertical-align: middle; margin: 0; font-weight: 400; + overflow-wrap: anywhere; } .tag-bar .tag-select .tag-select-box textarea { @@ -169,7 +169,7 @@ border-bottom: 0; } -.tag-bar-no-border .tag-select span { +.tag-bar-no-border .tag-select > span { border-right: 0; } @@ -186,7 +186,7 @@ padding-left: 0.625rem !important; } -.inline-row-tag-bar .tag-bar .tag-select span { +.inline-row-tag-bar .tag-bar .tag-select > span { padding: 0.3125rem; border-right: none; } diff --git a/app/controllers/kaui/accounts_controller.rb b/app/controllers/kaui/accounts_controller.rb index 569249005..0ee2c0bc4 100644 --- a/app/controllers/kaui/accounts_controller.rb +++ b/app/controllers/kaui/accounts_controller.rb @@ -156,7 +156,15 @@ def show @children = wait(fetch_children) @account_parent = @account.parent_account_id.nil? ? nil : wait(fetch_parent) @email_notification_configuration = is_email_notifications_plugin_available ? wait(fetch_email_notification_configuration) : [] - @bundles = wait(fetch_bundles) + @bundles = begin + wait(fetch_bundles) + rescue KillBillClient::API::ResponseError => e + # e.g. a subscription references a plan that no longer exists in the currently loaded catalog. + # Don't fail the whole account page for this, just warn and show it without bundle-derived data. + Rails.logger.warn("Unable to fetch bundles for account #{@account.account_id}: #{as_string(e)}") + flash.now[:warning] = "Unable to load some subscription details: #{as_string(e)}" + [] + end @last_transaction_by_payment_method_id = {} wait(fetch_payments).each do |payment| diff --git a/app/helpers/kaui/date_helper.rb b/app/helpers/kaui/date_helper.rb index 4fc155f1f..154724d09 100644 --- a/app/helpers/kaui/date_helper.rb +++ b/app/helpers/kaui/date_helper.rb @@ -24,6 +24,18 @@ def truncate_millis(date_s) DateTime.parse(date_s).strftime('%FT%T') end + # Best-effort epoch (seconds) timestamp for a timeline entry's date/time + # value, used purely for client-side "Sort by Date" ordering on the + # account timeline page. Returns nil if the date is missing or cannot be + # parsed (entries without a usable timestamp are sorted last). + def timeline_sort_timestamp(date) + return nil if date.blank? + + DateTime.parse(date.to_s).to_i + rescue ArgumentError, TypeError + nil + end + # Retrieve current killbill server time based on a time zone. # if no time zone is provided it will return UTC. # if the killbill server is not reachable it will return the time of the Kaui server diff --git a/app/services/dependencies/aviate.rb b/app/services/dependencies/aviate.rb index 4b0622ca2..e41860981 100644 --- a/app/services/dependencies/aviate.rb +++ b/app/services/dependencies/aviate.rb @@ -21,10 +21,13 @@ class << self # Any error (plugin not installed, missing/invalid JWT, network issue, etc.) is treated as # "not Aviate" so that callers can safely fall back to the existing XML-catalog behavior. def aviate_catalog?(account_id, options_for_klient) - response = KillBillClient::API.get("#{KILLBILL_AVIATE_PREFIX}/catalog/info", catalog_params(account_id), request_options(options_for_klient)) - JSON.parse(response.body)['state'] == CATALOG_STATE_AVIATE - rescue StandardError => e - Rails.logger.warn("Failed to retrieve Aviate catalog info: #{e.class}: #{e.message}") + params_list = account_id.present? ? [{ accountId: account_id }, {}] : [{}] + params_list.each do |params| + response = KillBillClient::API.get("#{KILLBILL_AVIATE_PREFIX}/catalog/info", params, request_options(options_for_klient)) + return true if JSON.parse(response.body)['state'] == CATALOG_STATE_AVIATE + rescue StandardError => e + Rails.logger.warn("Failed to retrieve Aviate catalog info (params=#{params}): #{e.class}: #{e.message}") + end false end diff --git a/app/views/kaui/account_timelines/show.html.erb b/app/views/kaui/account_timelines/show.html.erb index e121dcb5c..73032eefc 100644 --- a/app/views/kaui/account_timelines/show.html.erb +++ b/app/views/kaui/account_timelines/show.html.erb @@ -49,7 +49,14 @@ -
+
+
+ + <%= select_tag 'group-by', options_for_select([['Object Type', 'type'], ['Date', 'date']], 'type'), :class => 'form-control' %> +
+
+ +
<% if @account.present? %> <% @invoices.each do |invoice_stub| %> <% if invoice_stub.bundle_keys.present? %> @@ -60,7 +67,7 @@ <% else %> <% invoice = invoice_stub %> <% end %> -
+
<% if invoice.target_date.present? %> <%= invoice.target_date %> @@ -148,7 +155,7 @@ <% invoice = nil %> <% end %> <% payment.transactions.each do |transaction| %> -
+
<% if transaction.effective_date.present? %> <%= format_date(transaction.effective_date, @account.time_zone).html_safe %> @@ -229,7 +236,7 @@ <% bundle.subscriptions.each do |sub| %> <% sub.events.each do |event| %> <% next if event.event_type == 'SERVICE_STATE_CHANGE' %> -
+
<% if event.effective_date.present? %> <%= format_date(event.effective_date, @account.time_zone).html_safe %> @@ -352,6 +359,39 @@ }); } + var originalTimelineEntriesOrder = null; + + function sortTimelineEntries(sortByDate) { + var $container = $('#timeline-entries'); + var $entries = $container.children('[data-event-type]'); + + if (originalTimelineEntriesOrder === null) { + // Cache the initial (grouped-by-object-type) DOM order the first time we need it + originalTimelineEntriesOrder = $entries.get(); + } + + var orderedEntries; + if (sortByDate) { + orderedEntries = $entries.get().sort(function (a, b) { + var x = parseInt($(a).attr('data-timestamp'), 10); + var y = parseInt($(b).attr('data-timestamp'), 10); + x = isNaN(x) ? -Infinity : x; + y = isNaN(y) ? -Infinity : y; + // Most recent first; entries without a usable timestamp sort last + return y - x; + }); + } else { + orderedEntries = originalTimelineEntriesOrder; + } + + $.each(orderedEntries, function (_, entry) { + $container.append(entry); + }); + + // Re-apply any active filters, since the DOM order just changed + filterTimeline(); + } + function filterTimeline() { var selectedBundle = $("#bundles").val(); var selectedPayment = $('#payment_filter').val().toUpperCase(); @@ -424,6 +464,11 @@ clearTimeout(searchTimeout); searchTimeout = setTimeout(filterTimeline, 300); }); + + $('#group-by').on('change', function() { + sortTimelineEntries($(this).val() === 'date'); + }); + diff --git a/app/views/kaui/layouts/kaui_account_navbar.html.erb b/app/views/kaui/layouts/kaui_account_navbar.html.erb index 6f4575558..3356acb82 100644 --- a/app/views/kaui/layouts/kaui_account_navbar.html.erb +++ b/app/views/kaui/layouts/kaui_account_navbar.html.erb @@ -6,6 +6,7 @@ <%= link_to 'Account', kaui_engine.account_path(params[:account_id]), :class => (params[:controller].ends_with?('accounts') ? 'current' : 'non-current') %> <% if aviate_plugin_installed %> <%= link_to 'Wallets', aviate_engine.account_wallets_path(params[:account_id]), :class => (params[:controller].ends_with?('wallets') ? 'current' : 'non-current') %> + <%= link_to 'Usage', aviate_engine.account_usage_index_path(params[:account_id]), :class => (params[:controller].ends_with?('usage') ? 'current' : 'non-current') %> <% end %> <%= link_to 'Subscriptions', kaui_engine.account_bundles_path(params[:account_id]), :class => (params[:controller].ends_with?('bundles') ? 'current' : 'non-current') %> <%= link_to 'Invoices', kaui_engine.account_invoices_path(params[:account_id]), :class => (params[:controller].ends_with?('invoices') ? 'current' : 'non-current') %> diff --git a/app/views/kaui/layouts/kaui_account_sidebar.html.erb b/app/views/kaui/layouts/kaui_account_sidebar.html.erb index afef1f957..131926bb3 100644 --- a/app/views/kaui/layouts/kaui_account_sidebar.html.erb +++ b/app/views/kaui/layouts/kaui_account_sidebar.html.erb @@ -15,6 +15,7 @@ <% if aviate_plugin_installed %> <% menu_items << { label: 'Wallets', path: aviate_engine.account_wallets_path(params[:account_id]), controller_suffix: 'wallets', icon_path: 'kaui/sidebar/invoices.svg' } %> + <% menu_items << { label: 'Usage', path: aviate_engine.account_usage_index_path(params[:account_id]), controller_suffix: 'usage', icon_path: 'kaui/sidebar/subscriptions.svg' } %> <% end %> <% if current_user.root? %> diff --git a/app/views/kaui/subscriptions/_subscriptions_table.html.erb b/app/views/kaui/subscriptions/_subscriptions_table.html.erb index f88ef13ab..c8eafaf3f 100644 --- a/app/views/kaui/subscriptions/_subscriptions_table.html.erb +++ b/app/views/kaui/subscriptions/_subscriptions_table.html.erb @@ -215,6 +215,14 @@ path: kaui_engine.record_usage_path(id: sub.subscription_id), icon: "kaui/subscription/change.svg" } %> + + <% if aviate_plugin_installed %> + <% menu_items << { + label: "View Usage", + path: aviate_engine.usage_subscription_path(id: sub.subscription_id), + icon: "kaui/subscription/change.svg" + } %> + <% end %> <% end %> <% if can?(:transfer, Kaui::Bundle) && sub.product_category != 'ADD_ON' %>