kaui 4.0.18 → 4.0.20
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 +4 -4
- data/app/controllers/kaui/admin_tenants_controller.rb +41 -3
- data/app/controllers/kaui/subscriptions_controller.rb +129 -31
- data/app/models/kaui/admin_tenant.rb +4 -0
- data/app/views/kaui/admin_tenants/_form_catalog_translation.erb +1 -1
- data/app/views/kaui/admin_tenants/_form_invoice_template.erb +23 -2
- data/app/views/kaui/admin_tenants/_form_invoice_translation.erb +1 -1
- data/app/views/kaui/bundles/index.html.erb +2 -2
- data/app/views/kaui/components/menu_dropdown/_menu_dropdown.html.erb +8 -14
- data/app/views/kaui/invoices/_multi_functions_bar.html.erb +8 -9
- data/app/views/kaui/payments/_multi_functions_bar.html.erb +8 -9
- data/app/views/kaui/subscriptions/_edit_form.html.erb +147 -3
- data/app/views/kaui/subscriptions/_form.html.erb +147 -3
- data/app/views/kaui/subscriptions/record_usage.erb +74 -28
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +1 -0
- data/lib/kaui/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ef67e5aac2cf29cee71a7a52b5b87452afc410ed711d1d55c08225dc39b1d044
|
|
4
|
+
data.tar.gz: 6232e9397218afea690a6d45422623b16fb346489a380eaaa6fdba3ed6e98930
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2cd48b2725369b2a466e49cabbf656ce64b6bea4cdcb272f97fb7d34c909bdb56a6876c13af14328a1dcdd0d341e9d04757318d9504aeece87d55ad2ba8a6fa
|
|
7
|
+
data.tar.gz: 8abadbb98b2d800acb54b5dd7fd9bdcf20a8a49186c1b3b9b2357eb3a717ff6a79a45094aebcfaa252a11a8ca562ac5ab2812080c6abb3774ea711b57d98099b
|
|
@@ -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
|
|
@@ -14,6 +14,7 @@ module Kaui
|
|
|
14
14
|
|
|
15
15
|
@bundle, plans_details = lookup_bundle_and_plan_details(@subscription, @base_product_name)
|
|
16
16
|
@plans = plans_details.map(&:plan)
|
|
17
|
+
@plan_phases = build_plan_phases_map(plans_details)
|
|
17
18
|
|
|
18
19
|
return unless @plans.empty?
|
|
19
20
|
|
|
@@ -30,6 +31,7 @@ module Kaui
|
|
|
30
31
|
_, plans_details = lookup_bundle_and_plan_details(@subscription)
|
|
31
32
|
# Use a Set to deal with multiple pricelists
|
|
32
33
|
@plans = Set.new.merge(plans_details.map(&:plan))
|
|
34
|
+
@plan_phases = build_plan_phases_map(plans_details)
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def create
|
|
@@ -46,15 +48,7 @@ module Kaui
|
|
|
46
48
|
@subscription.plan_name = plan_name
|
|
47
49
|
requested_date = params[:type_change] == 'DATE' ? params[:requested_date].presence : nil
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
override_fixed_price = begin
|
|
51
|
-
plan_details.phases.first.prices.blank?
|
|
52
|
-
rescue StandardError
|
|
53
|
-
false
|
|
54
|
-
end
|
|
55
|
-
override_recurring_price = !override_fixed_price
|
|
56
|
-
phase_type = @bundle.nil? ? plan_details.phases.first.type : @bundle.subscriptions.first.phase_type
|
|
57
|
-
overrides = price_overrides(phase_type, override_fixed_price, override_recurring_price)
|
|
51
|
+
overrides = price_overrides(plan_details)
|
|
58
52
|
@subscription.price_overrides = overrides if overrides.present?
|
|
59
53
|
@subscription.quantity = params[:quantity].to_i if params[:quantity].present? && params[:quantity].to_i.positive?
|
|
60
54
|
|
|
@@ -95,11 +89,9 @@ module Kaui
|
|
|
95
89
|
|
|
96
90
|
input = { planName: plan_name }
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
override_recurring_price = !override_fixed_price
|
|
102
|
-
overrides = price_overrides(subscription.phase_type, override_fixed_price, override_recurring_price)
|
|
92
|
+
_, plans_details = lookup_bundle_and_plan_details(subscription)
|
|
93
|
+
plan_details = plans_details.find { |p| p.plan == plan_name }
|
|
94
|
+
overrides = plan_details ? price_overrides(plan_details) : nil
|
|
103
95
|
input[:priceOverrides] = overrides if overrides.present?
|
|
104
96
|
|
|
105
97
|
subscription.change_plan(input,
|
|
@@ -189,6 +181,7 @@ module Kaui
|
|
|
189
181
|
|
|
190
182
|
def record_usage
|
|
191
183
|
@subscription = Kaui::Subscription.find_by_id(params.require(:id), 'NONE', options_for_klient)
|
|
184
|
+
@unit_types = fetch_unit_types_from_subscription(@subscription)
|
|
192
185
|
end
|
|
193
186
|
|
|
194
187
|
def create_usage
|
|
@@ -204,16 +197,13 @@ module Kaui
|
|
|
204
197
|
amount = Integer(amount_raw, exception: false)
|
|
205
198
|
errors << 'Amount must be a positive integer' if amount.nil? || amount <= 0
|
|
206
199
|
errors << 'Date/time of usage is required' if record_date.blank?
|
|
207
|
-
parsed_date =
|
|
208
|
-
|
|
209
|
-
rescue ArgumentError
|
|
210
|
-
nil
|
|
211
|
-
end
|
|
212
|
-
errors << 'Date/time of usage must be a valid ISO 8601 timestamp' if record_date.present? && parsed_date.nil?
|
|
200
|
+
parsed_date = parse_usage_date(record_date) if record_date.present?
|
|
201
|
+
errors << 'Date/time of usage must be a valid date or datetime' if record_date.present? && parsed_date.nil?
|
|
213
202
|
|
|
214
203
|
if errors.any?
|
|
215
204
|
flash.now[:error] = errors.join('. ')
|
|
216
205
|
@subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
|
|
206
|
+
@unit_types = fetch_unit_types_from_subscription(@subscription)
|
|
217
207
|
@unit_type = unit_type
|
|
218
208
|
@amount = amount_raw
|
|
219
209
|
@record_date = record_date
|
|
@@ -235,7 +225,7 @@ module Kaui
|
|
|
235
225
|
usage.tracking_id = params[:tracking_id].presence
|
|
236
226
|
usage.unit_usage_records = [unit_usage_record]
|
|
237
227
|
|
|
238
|
-
usage.create(current_user.kb_username,
|
|
228
|
+
usage.create(current_user.kb_username, nil, nil, options_for_klient)
|
|
239
229
|
|
|
240
230
|
subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
|
|
241
231
|
redirect_to kaui_engine.account_bundles_path(subscription.account_id), notice: 'Usage was successfully recorded'
|
|
@@ -244,6 +234,7 @@ module Kaui
|
|
|
244
234
|
Rails.logger.error(e.backtrace.join("\n")) if e.backtrace
|
|
245
235
|
flash.now[:error] = "Error while recording usage: #{as_string(e)}"
|
|
246
236
|
@subscription = Kaui::Subscription.find_by_id(subscription_id, 'NONE', options_for_klient)
|
|
237
|
+
@unit_types = fetch_unit_types_from_subscription(@subscription)
|
|
247
238
|
@unit_type = unit_type
|
|
248
239
|
@amount = amount_raw
|
|
249
240
|
@record_date = record_date
|
|
@@ -357,19 +348,126 @@ module Kaui
|
|
|
357
348
|
plans
|
|
358
349
|
end
|
|
359
350
|
|
|
360
|
-
def price_overrides(
|
|
361
|
-
|
|
351
|
+
def price_overrides(plan_details)
|
|
352
|
+
raw = params[:price_overrides]
|
|
353
|
+
return nil if raw.blank?
|
|
354
|
+
|
|
355
|
+
entries = raw.respond_to?(:values) ? raw.values : Array(raw)
|
|
356
|
+
phase_meta = plan_details.respond_to?(:phases) ? (plan_details.phases || []).index_by(&:type) : {}
|
|
357
|
+
|
|
358
|
+
overrides = entries.filter_map do |entry|
|
|
359
|
+
entry = entry.to_unsafe_h if entry.respond_to?(:to_unsafe_h)
|
|
360
|
+
entry = entry.with_indifferent_access if entry.respond_to?(:with_indifferent_access)
|
|
361
|
+
|
|
362
|
+
price_str = entry[:price].to_s.strip
|
|
363
|
+
phase_type = entry[:phase_type].to_s.strip
|
|
364
|
+
|
|
365
|
+
price = BigDecimal(price_str, exception: false)
|
|
366
|
+
|
|
367
|
+
next if price.nil? || phase_type.blank? || price.negative?
|
|
368
|
+
|
|
369
|
+
phase = phase_meta[phase_type]
|
|
370
|
+
next if phase.nil?
|
|
371
|
+
|
|
372
|
+
override = KillBillClient::Model::PhasePriceAttributes.new
|
|
373
|
+
override.phase_type = phase_type
|
|
374
|
+
if phase_uses_fixed_price?(phase)
|
|
375
|
+
override.fixed_price = price.to_s('F')
|
|
376
|
+
else
|
|
377
|
+
override.recurring_price = price.to_s('F')
|
|
378
|
+
end
|
|
379
|
+
override
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
overrides.presence
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def build_plan_phases_map(plans_details)
|
|
386
|
+
(plans_details || []).to_h do |pd|
|
|
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
|
|
406
|
+
[pd.plan, phases]
|
|
407
|
+
end
|
|
408
|
+
end
|
|
362
409
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
override.phase_type = phase_type
|
|
367
|
-
override.fixed_price = price_override if override_fixed_price
|
|
368
|
-
override.recurring_price = price_override if override_recurring_price
|
|
410
|
+
def phase_uses_fixed_price?(phase)
|
|
411
|
+
(phase.prices || []).empty?
|
|
412
|
+
end
|
|
369
413
|
|
|
370
|
-
|
|
414
|
+
def fetch_unit_types_from_subscription(subscription)
|
|
415
|
+
unit_types = []
|
|
416
|
+
(subscription.prices || []).each do |phase_price|
|
|
417
|
+
usage_prices = if phase_price.is_a?(Hash)
|
|
418
|
+
phase_price['usagePrices'] || phase_price[:usagePrices] || []
|
|
419
|
+
elsif phase_price.respond_to?(:usage_prices)
|
|
420
|
+
phase_price.usage_prices || []
|
|
421
|
+
else
|
|
422
|
+
[]
|
|
423
|
+
end
|
|
424
|
+
(usage_prices || []).each do |usage_price|
|
|
425
|
+
tier_prices = if usage_price.is_a?(Hash)
|
|
426
|
+
usage_price['tierPrices'] || usage_price[:tierPrices] || []
|
|
427
|
+
elsif usage_price.respond_to?(:tier_prices)
|
|
428
|
+
usage_price.tier_prices || []
|
|
429
|
+
else
|
|
430
|
+
[]
|
|
431
|
+
end
|
|
432
|
+
(tier_prices || []).each do |tier_price|
|
|
433
|
+
block_prices = if tier_price.is_a?(Hash)
|
|
434
|
+
tier_price['blockPrices'] || tier_price[:blockPrices] || []
|
|
435
|
+
elsif tier_price.respond_to?(:block_prices)
|
|
436
|
+
tier_price.block_prices || []
|
|
437
|
+
else
|
|
438
|
+
[]
|
|
439
|
+
end
|
|
440
|
+
(block_prices || []).each do |block_price|
|
|
441
|
+
unit_name = if block_price.is_a?(Hash)
|
|
442
|
+
block_price['unitName'] || block_price[:unitName] || block_price['unit_name']
|
|
443
|
+
elsif block_price.respond_to?(:unit_name)
|
|
444
|
+
block_price.unit_name
|
|
445
|
+
end
|
|
446
|
+
unit_types << unit_name if unit_name.present?
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
unit_types.uniq
|
|
452
|
+
rescue StandardError => e
|
|
453
|
+
Rails.logger.warn("Failed to extract unit types from subscription #{subscription&.subscription_id}: #{e.class}: #{e.message}")
|
|
454
|
+
[]
|
|
455
|
+
end
|
|
371
456
|
|
|
372
|
-
|
|
457
|
+
def parse_usage_date(str)
|
|
458
|
+
str = str.to_s.strip
|
|
459
|
+
return nil if str.empty?
|
|
460
|
+
|
|
461
|
+
if str.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
462
|
+
year, month, day = str.split('-').map(&:to_i)
|
|
463
|
+
Time.utc(year, month, day)
|
|
464
|
+
elsif str.match?(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
|
|
465
|
+
Time.iso8601("#{str}:00Z")
|
|
466
|
+
else
|
|
467
|
+
Time.iso8601(str)
|
|
468
|
+
end
|
|
469
|
+
rescue ArgumentError
|
|
470
|
+
nil
|
|
373
471
|
end
|
|
374
472
|
end
|
|
375
473
|
end
|
|
@@ -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
|
|
@@ -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">
|
|
@@ -61,9 +61,9 @@
|
|
|
61
61
|
<% end %>
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
<% if @bundles.empty? %>
|
|
64
|
+
<% if @bundles.empty? && @search_query.present? %>
|
|
65
65
|
<div class="custom-alert custom-alert-info mt-3">
|
|
66
|
-
<span>No bundles found
|
|
66
|
+
<span>No bundles found for the given search query.</span>
|
|
67
67
|
</div>
|
|
68
68
|
<% end %>
|
|
69
69
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<ul class="dropdown-menu header-menu shadow border-0 rounded-3"
|
|
19
19
|
id="<%= dropdown_id %>_menu"
|
|
20
|
+
aria-labelledby="<%= dropdown_id %>_button"
|
|
20
21
|
style="min-width: 6.25rem; display: none;">
|
|
21
22
|
<% menu_items&.each do |item| %>
|
|
22
23
|
<li>
|
|
@@ -31,28 +32,21 @@
|
|
|
31
32
|
|
|
32
33
|
<%= javascript_tag do %>
|
|
33
34
|
document.addEventListener('DOMContentLoaded', function () {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
var button = document.getElementById('<%= dropdown_id %>_button');
|
|
36
|
+
var menu = document.getElementById('<%= dropdown_id %>_menu');
|
|
37
|
+
var wrapper = document.getElementById('<%= dropdown_id %>_wrapper');
|
|
37
38
|
|
|
38
39
|
button.addEventListener('click', function (e) {
|
|
39
40
|
e.stopPropagation();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Close all other open dropdowns first (including other dots menus)
|
|
43
|
-
document.querySelectorAll('.dropdown-menu').forEach(function (m) {
|
|
44
|
-
if (m !== menu) {
|
|
45
|
-
m.style.display = 'none';
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Toggle this one
|
|
41
|
+
var isShown = menu.style.display === 'block';
|
|
50
42
|
menu.style.display = isShown ? 'none' : 'block';
|
|
43
|
+
button.setAttribute('aria-expanded', !isShown);
|
|
51
44
|
});
|
|
52
45
|
|
|
53
46
|
document.addEventListener('click', function (e) {
|
|
54
|
-
if (!wrapper.contains(e.target)) {
|
|
47
|
+
if (!wrapper.contains(e.target) && menu.style.display === 'block') {
|
|
55
48
|
menu.style.display = 'none';
|
|
49
|
+
button.setAttribute('aria-expanded', 'false');
|
|
56
50
|
}
|
|
57
51
|
});
|
|
58
52
|
});
|
|
@@ -45,10 +45,7 @@
|
|
|
45
45
|
type: "button",
|
|
46
46
|
html_class: "kaui-button custom-hover",
|
|
47
47
|
html_options: {
|
|
48
|
-
id: "dropdownMenu1"
|
|
49
|
-
data: {
|
|
50
|
-
bs_toggle: "dropdown"
|
|
51
|
-
}
|
|
48
|
+
id: "dropdownMenu1"
|
|
52
49
|
}
|
|
53
50
|
} %>
|
|
54
51
|
<ul class="dropdown-menu" id="column-visibility" aria-labelledby="dropdownMenu1">
|
|
@@ -213,13 +210,15 @@
|
|
|
213
210
|
|
|
214
211
|
<%= javascript_tag do %>
|
|
215
212
|
$(document).ready(function() {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
// Toggle Edit Columns dropdown
|
|
214
|
+
$('#dropdownMenu1').on('click', function(e) {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
var $menu = $('#column-visibility');
|
|
217
|
+
$menu.toggleClass('show');
|
|
218
|
+
$(this).attr('aria-expanded', $menu.hasClass('show'));
|
|
220
219
|
});
|
|
221
220
|
|
|
222
|
-
$('
|
|
221
|
+
$('#column-visibility').on('click', 'input[type="checkbox"], label', function(event) {
|
|
223
222
|
event.stopPropagation();
|
|
224
223
|
});
|
|
225
224
|
|
|
@@ -45,10 +45,7 @@
|
|
|
45
45
|
type: "button",
|
|
46
46
|
html_class: "kaui-button custom-hover",
|
|
47
47
|
html_options: {
|
|
48
|
-
id: "dropdownMenu1"
|
|
49
|
-
data: {
|
|
50
|
-
bs_toggle: "dropdown"
|
|
51
|
-
}
|
|
48
|
+
id: "dropdownMenu1"
|
|
52
49
|
}
|
|
53
50
|
} %>
|
|
54
51
|
<ul class="dropdown-menu" id="column-visibility" aria-labelledby="dropdownMenu1">
|
|
@@ -213,13 +210,15 @@
|
|
|
213
210
|
|
|
214
211
|
<%= javascript_tag do %>
|
|
215
212
|
$(document).ready(function() {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
213
|
+
// Toggle Edit Columns dropdown
|
|
214
|
+
$('#dropdownMenu1').on('click', function(e) {
|
|
215
|
+
e.stopPropagation();
|
|
216
|
+
var $menu = $('#column-visibility');
|
|
217
|
+
$menu.toggleClass('show');
|
|
218
|
+
$(this).attr('aria-expanded', $menu.hasClass('show'));
|
|
220
219
|
});
|
|
221
220
|
|
|
222
|
-
$('
|
|
221
|
+
$('#column-visibility').on('click', 'input[type="checkbox"], label', function(event) {
|
|
223
222
|
event.stopPropagation();
|
|
224
223
|
});
|
|
225
224
|
|
|
@@ -7,9 +7,49 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
9
|
<div class="form-group d-flex pb-3 border-bottom mb-3">
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
<div class="col-sm-3 control-label" style="padding-top: 2px;">
|
|
11
|
+
<div id="toggle_phase_overrides" style="cursor: pointer; user-select: none;">
|
|
12
|
+
<span id="toggle_phase_overrides_caret">▾</span>
|
|
13
|
+
<strong>Phase Overrides</strong>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="col-sm-9" id="phase_overrides_section">
|
|
17
|
+
<p class="text-muted mb-2" style="font-size: 13px;">Override prices for specific phases. All other phases will use catalog pricing.</p>
|
|
18
|
+
<div id="phase_overrides_headers" class="d-flex mb-1 gap-2" style="display: none;">
|
|
19
|
+
<div style="flex: 1; padding-left: 2px;"><small><strong>Phase</strong></small></div>
|
|
20
|
+
<div style="flex: 1; padding-left: 2px;">
|
|
21
|
+
<small><strong>Override Price</strong></small>
|
|
22
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle; margin-left: 3px;" title="Leave blank to use catalog pricing">
|
|
23
|
+
<circle cx="8" cy="8" r="7" stroke="#A4A7AE" stroke-width="1.25"/>
|
|
24
|
+
<path d="M8 7v5M8 5.5v-.5" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round"/>
|
|
25
|
+
</svg>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="width: 40px;"></div>
|
|
28
|
+
</div>
|
|
29
|
+
<div id="phase_overrides_rows"></div>
|
|
30
|
+
<button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
|
|
31
|
+
style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
|
|
32
|
+
<span style="font-size: 16px; line-height: 1;">+</span> Add another phase
|
|
33
|
+
</button>
|
|
34
|
+
<script type="application/json" id="plan_phases_data"><%= raw json_escape((@plan_phases || {}).to_json) %></script>
|
|
35
|
+
<style>.phase-override-remove:hover svg path { stroke: #D92D20; }</style>
|
|
36
|
+
<template id="phase_override_row_template">
|
|
37
|
+
<div class="phase-override-row d-flex align-items-center mb-2 gap-2">
|
|
38
|
+
<select name="price_overrides[][phase_type]" class="form-control phase-override-type" style="flex: 1;"></select>
|
|
39
|
+
<div class="input-group phase-override-price-group" style="flex: 1;">
|
|
40
|
+
<span class="input-group-text">$</span>
|
|
41
|
+
<input type="number" name="price_overrides[][price]" step="any" min="0" placeholder="0.00" class="form-control phase-override-price">
|
|
42
|
+
</div>
|
|
43
|
+
<button type="button" class="btn btn-outline-secondary phase-override-remove" style="padding: 6px 10px; flex-shrink: 0;" aria-label="Remove">
|
|
44
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
45
|
+
<path d="M13 3.66699L12.4093 13.4143C12.3666 14.1181 11.7834 14.667 11.0783 14.667H4.92164C4.21659 14.667 3.6334 14.1181 3.59075 13.4143L3 3.66699" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
46
|
+
<path d="M2 3.66634H5.33333M5.33333 3.66634L6.16017 1.73706C6.26522 1.49194 6.50625 1.33301 6.77293 1.33301H9.22707C9.49373 1.33301 9.7348 1.49194 9.8398 1.73706L10.6667 3.66634M5.33333 3.66634H10.6667M14 3.66634H10.6667" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
47
|
+
<path d="M6.33325 11V7" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
48
|
+
<path d="M9.66675 11V7" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
49
|
+
</svg>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
13
53
|
</div>
|
|
14
54
|
</div>
|
|
15
55
|
<div class="form-group d-flex pb-3" id="radio_group">
|
|
@@ -106,5 +146,109 @@ $(document).ready(function() {
|
|
|
106
146
|
$( "input[type=radio]").on("click", function() {
|
|
107
147
|
onTypeChange();
|
|
108
148
|
});
|
|
149
|
+
initPhaseOverrides();
|
|
109
150
|
});
|
|
151
|
+
|
|
152
|
+
function initPhaseOverrides() {
|
|
153
|
+
var planPhases = {};
|
|
154
|
+
try { planPhases = JSON.parse($('#plan_phases_data').text() || '{}'); } catch (e) { planPhases = {}; }
|
|
155
|
+
|
|
156
|
+
var $section = $('#phase_overrides_section');
|
|
157
|
+
var $rows = $('#phase_overrides_rows');
|
|
158
|
+
var $addBtn = $('#add_phase_override');
|
|
159
|
+
var $headers = $('#phase_overrides_headers');
|
|
160
|
+
var $template = $('#phase_override_row_template');
|
|
161
|
+
var $planSelect = $('select[name="plan_name"]');
|
|
162
|
+
|
|
163
|
+
function currentPhases() {
|
|
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
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function phaseLabel(p) {
|
|
174
|
+
var title = p.type.charAt(0) + p.type.slice(1).toLowerCase();
|
|
175
|
+
return p.priceLabel ? title + ' (' + p.priceLabel + ')' : title;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function selectedTypes($exceptRow) {
|
|
179
|
+
return $rows.find('.phase-override-row').filter(function() {
|
|
180
|
+
return !$exceptRow || this !== $exceptRow[0];
|
|
181
|
+
}).map(function() { return $(this).find('.phase-override-type').val(); }).get();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function refreshAddBtn() {
|
|
185
|
+
var phases = currentPhases();
|
|
186
|
+
var used = selectedTypes().filter(function(type) { return type; });
|
|
187
|
+
var hasMore = phases.length > 0 && used.length < phases.length;
|
|
188
|
+
if (hasMore) {
|
|
189
|
+
$addBtn.css('display', 'flex');
|
|
190
|
+
} else {
|
|
191
|
+
$addBtn.hide();
|
|
192
|
+
}
|
|
193
|
+
$headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function rebuildRowOptions() {
|
|
197
|
+
var phases = currentPhases();
|
|
198
|
+
$rows.find('.phase-override-row').each(function() {
|
|
199
|
+
var $row = $(this);
|
|
200
|
+
var $select = $row.find('.phase-override-type');
|
|
201
|
+
var current = $select.val();
|
|
202
|
+
var used = selectedTypes($row);
|
|
203
|
+
$select.empty();
|
|
204
|
+
phases.forEach(function(p) {
|
|
205
|
+
if (used.indexOf(p.type) === -1) {
|
|
206
|
+
$select.append($('<option>', { value: p.type, text: phaseLabel(p) }));
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
var currentStillValid = phases.some(function(p) { return p.type === current; }) && used.indexOf(current) === -1;
|
|
210
|
+
if (currentStillValid) $select.val(current);
|
|
211
|
+
});
|
|
212
|
+
refreshAddBtn();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function addRow(preferredType) {
|
|
216
|
+
var phases = currentPhases();
|
|
217
|
+
if (phases.length === 0) return;
|
|
218
|
+
var used = selectedTypes().filter(function(type) { return type; });
|
|
219
|
+
var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
|
|
220
|
+
? preferredType
|
|
221
|
+
: (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
|
|
222
|
+
if (!next) return;
|
|
223
|
+
|
|
224
|
+
var $row = $($template.html());
|
|
225
|
+
$rows.append($row);
|
|
226
|
+
rebuildRowOptions();
|
|
227
|
+
$row.find('.phase-override-type').val(next);
|
|
228
|
+
rebuildRowOptions();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resetRows() {
|
|
232
|
+
$rows.empty();
|
|
233
|
+
var phases = currentPhases();
|
|
234
|
+
if (phases.length > 0) addRow(phases[0].type);
|
|
235
|
+
refreshAddBtn();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
$('#toggle_phase_overrides').on('click', function() {
|
|
239
|
+
$section.toggle();
|
|
240
|
+
$('#toggle_phase_overrides_caret').html($section.is(':visible') ? '▾' : '▸');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
$addBtn.on('click', function() { addRow(); });
|
|
244
|
+
|
|
245
|
+
$rows.on('change', '.phase-override-type', rebuildRowOptions);
|
|
246
|
+
$rows.on('click', '.phase-override-remove', function() {
|
|
247
|
+
$(this).closest('.phase-override-row').remove();
|
|
248
|
+
refreshAddBtn();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
$planSelect.on('change', resetRows);
|
|
252
|
+
resetRows();
|
|
253
|
+
}
|
|
110
254
|
<% end %>
|
|
@@ -35,9 +35,49 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
</div>
|
|
37
37
|
<div class="form-group d-flex pb-3">
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
<div class="col-sm-3 control-label" style="padding-top: 2px;">
|
|
39
|
+
<div id="toggle_phase_overrides" style="cursor: pointer; user-select: none;">
|
|
40
|
+
<span id="toggle_phase_overrides_caret">▸</span>
|
|
41
|
+
<strong>Price Override</strong>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="col-sm-9" id="phase_overrides_section" style="display: none;">
|
|
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
|
+
<div id="phase_overrides_headers" class="d-flex mb-1 gap-2" style="display: none;">
|
|
47
|
+
<div style="flex: 1; padding-left: 2px;"><small><strong>Phase</strong></small></div>
|
|
48
|
+
<div style="flex: 1; padding-left: 2px;">
|
|
49
|
+
<small><strong>Override Price</strong></small>
|
|
50
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle; margin-left: 3px;" title="Leave blank to use catalog pricing">
|
|
51
|
+
<circle cx="8" cy="8" r="7" stroke="#A4A7AE" stroke-width="1.25"/>
|
|
52
|
+
<path d="M8 7v5M8 5.5v-.5" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round"/>
|
|
53
|
+
</svg>
|
|
54
|
+
</div>
|
|
55
|
+
<div style="width: 40px;"></div>
|
|
56
|
+
</div>
|
|
57
|
+
<div id="phase_overrides_rows"></div>
|
|
58
|
+
<button type="button" id="add_phase_override" class="btn w-100 mt-2 align-items-center justify-content-center gap-1"
|
|
59
|
+
style="border: 1px dashed #d0d5dd; background: white; color: #2563eb; font-size: 14px;">
|
|
60
|
+
<span style="font-size: 16px; line-height: 1;">+</span> Add another phase
|
|
61
|
+
</button>
|
|
62
|
+
<script type="application/json" id="plan_phases_data"><%= raw json_escape((@plan_phases || {}).to_json) %></script>
|
|
63
|
+
<style>.phase-override-remove:hover svg path { stroke: #D92D20; }</style>
|
|
64
|
+
<template id="phase_override_row_template">
|
|
65
|
+
<div class="phase-override-row d-flex align-items-center mb-2 gap-2">
|
|
66
|
+
<select name="price_overrides[][phase_type]" class="form-control phase-override-type" style="flex: 1;"></select>
|
|
67
|
+
<div class="input-group phase-override-price-group" style="flex: 1;">
|
|
68
|
+
<span class="input-group-text">$</span>
|
|
69
|
+
<input type="number" name="price_overrides[][price]" step="any" min="0" placeholder="0.00" class="form-control phase-override-price">
|
|
70
|
+
</div>
|
|
71
|
+
<button type="button" class="btn btn-outline-secondary phase-override-remove" style="padding: 6px 10px; flex-shrink: 0;" aria-label="Remove">
|
|
72
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
73
|
+
<path d="M13 3.66699L12.4093 13.4143C12.3666 14.1181 11.7834 14.667 11.0783 14.667H4.92164C4.21659 14.667 3.6334 14.1181 3.59075 13.4143L3 3.66699" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
74
|
+
<path d="M2 3.66634H5.33333M5.33333 3.66634L6.16017 1.73706C6.26522 1.49194 6.50625 1.33301 6.77293 1.33301H9.22707C9.49373 1.33301 9.7348 1.49194 9.8398 1.73706L10.6667 3.66634M5.33333 3.66634H10.6667M14 3.66634H10.6667" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
75
|
+
<path d="M6.33325 11V7" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
76
|
+
<path d="M9.66675 11V7" stroke="#A4A7AE" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
|
77
|
+
</svg>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</template>
|
|
41
81
|
</div>
|
|
42
82
|
</div>
|
|
43
83
|
<div class="form-group d-flex pb-3 border-bottom mb-3">
|
|
@@ -123,5 +163,109 @@ $(document).ready(function() {
|
|
|
123
163
|
$( "input[type=radio]").on("click", function() {
|
|
124
164
|
onTypeChange();
|
|
125
165
|
});
|
|
166
|
+
initPhaseOverrides();
|
|
126
167
|
});
|
|
168
|
+
|
|
169
|
+
function initPhaseOverrides() {
|
|
170
|
+
var planPhases = {};
|
|
171
|
+
try { planPhases = JSON.parse($('#plan_phases_data').text() || '{}'); } catch (e) { planPhases = {}; }
|
|
172
|
+
|
|
173
|
+
var $section = $('#phase_overrides_section');
|
|
174
|
+
var $rows = $('#phase_overrides_rows');
|
|
175
|
+
var $addBtn = $('#add_phase_override');
|
|
176
|
+
var $headers = $('#phase_overrides_headers');
|
|
177
|
+
var $template = $('#phase_override_row_template');
|
|
178
|
+
var $planSelect = $('select[name="plan_name"]');
|
|
179
|
+
|
|
180
|
+
function currentPhases() {
|
|
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
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function phaseLabel(p) {
|
|
191
|
+
var title = p.type.charAt(0) + p.type.slice(1).toLowerCase();
|
|
192
|
+
return p.priceLabel ? title + ' (' + p.priceLabel + ')' : title;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function selectedTypes($exceptRow) {
|
|
196
|
+
return $rows.find('.phase-override-row').filter(function() {
|
|
197
|
+
return !$exceptRow || this !== $exceptRow[0];
|
|
198
|
+
}).map(function() { return $(this).find('.phase-override-type').val(); }).get();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function refreshAddBtn() {
|
|
202
|
+
var phases = currentPhases();
|
|
203
|
+
var used = selectedTypes().filter(function(type) { return type; });
|
|
204
|
+
var hasMore = phases.length > 0 && used.length < phases.length;
|
|
205
|
+
if (hasMore) {
|
|
206
|
+
$addBtn.css('display', 'flex');
|
|
207
|
+
} else {
|
|
208
|
+
$addBtn.hide();
|
|
209
|
+
}
|
|
210
|
+
$headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function rebuildRowOptions() {
|
|
214
|
+
var phases = currentPhases();
|
|
215
|
+
$rows.find('.phase-override-row').each(function() {
|
|
216
|
+
var $row = $(this);
|
|
217
|
+
var $select = $row.find('.phase-override-type');
|
|
218
|
+
var current = $select.val();
|
|
219
|
+
var used = selectedTypes($row);
|
|
220
|
+
$select.empty();
|
|
221
|
+
phases.forEach(function(p) {
|
|
222
|
+
if (used.indexOf(p.type) === -1) {
|
|
223
|
+
$select.append($('<option>', { value: p.type, text: phaseLabel(p) }));
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
var currentStillValid = phases.some(function(p) { return p.type === current; }) && used.indexOf(current) === -1;
|
|
227
|
+
if (currentStillValid) $select.val(current);
|
|
228
|
+
});
|
|
229
|
+
refreshAddBtn();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function addRow(preferredType) {
|
|
233
|
+
var phases = currentPhases();
|
|
234
|
+
if (phases.length === 0) return;
|
|
235
|
+
var used = selectedTypes().filter(function(type) { return type; });
|
|
236
|
+
var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
|
|
237
|
+
? preferredType
|
|
238
|
+
: (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
|
|
239
|
+
if (!next) return;
|
|
240
|
+
|
|
241
|
+
var $row = $($template.html());
|
|
242
|
+
$rows.append($row);
|
|
243
|
+
rebuildRowOptions();
|
|
244
|
+
$row.find('.phase-override-type').val(next);
|
|
245
|
+
rebuildRowOptions();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resetRows() {
|
|
249
|
+
$rows.empty();
|
|
250
|
+
var phases = currentPhases();
|
|
251
|
+
if (phases.length > 0) addRow(phases[0].type);
|
|
252
|
+
refreshAddBtn();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
$('#toggle_phase_overrides').on('click', function() {
|
|
256
|
+
$section.toggle();
|
|
257
|
+
$('#toggle_phase_overrides_caret').html($section.is(':visible') ? '▾' : '▸');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
$addBtn.on('click', function() { addRow(); });
|
|
261
|
+
|
|
262
|
+
$rows.on('change', '.phase-override-type', rebuildRowOptions);
|
|
263
|
+
$rows.on('click', '.phase-override-remove', function() {
|
|
264
|
+
$(this).closest('.phase-override-row').remove();
|
|
265
|
+
refreshAddBtn();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
$planSelect.on('change', resetRows);
|
|
269
|
+
resetRows();
|
|
270
|
+
}
|
|
127
271
|
<% end %>
|
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
<div class="form-group d-flex pb-3">
|
|
25
25
|
<%= label_tag :unit_type, 'Unit Type', class: 'col-sm-3 control-label' %>
|
|
26
26
|
<div class="col-sm-9">
|
|
27
|
-
|
|
27
|
+
<% if @unit_types.length == 1 %>
|
|
28
|
+
<%= text_field_tag :unit_type, @unit_types.first, readonly: true, required: true, class: 'form-control bg-light' %>
|
|
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-control', title: (@unit_type.presence || @unit_types.first) %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<%= text_field_tag :unit_type, @unit_type, required: true, maxlength: 255, class: 'form-control', placeholder: 'e.g. api-calls' %>
|
|
33
|
+
<% end %>
|
|
28
34
|
</div>
|
|
29
35
|
</div>
|
|
30
36
|
|
|
@@ -38,12 +44,40 @@
|
|
|
38
44
|
<div class="form-group d-flex pb-3">
|
|
39
45
|
<%= label_tag :record_date, 'Date/Time of Usage', class: 'col-sm-3 control-label' %>
|
|
40
46
|
<div class="col-sm-9">
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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>
|
|
47
81
|
</div>
|
|
48
82
|
</div>
|
|
49
83
|
|
|
@@ -54,20 +88,6 @@
|
|
|
54
88
|
</div>
|
|
55
89
|
</div>
|
|
56
90
|
|
|
57
|
-
<div class="form-group d-flex pb-3 border-bottom mb-3">
|
|
58
|
-
<%= label_tag :reason, 'Reason', class: 'col-sm-3 control-label' %>
|
|
59
|
-
<div class="col-sm-9">
|
|
60
|
-
<%= text_field_tag :reason, params[:reason], class: 'form-control' %>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
<div class="form-group d-flex pb-3 border-bottom mb-3">
|
|
65
|
-
<%= label_tag :comment, 'Comment', class: 'col-sm-3 control-label' %>
|
|
66
|
-
<div class="col-sm-9">
|
|
67
|
-
<%= text_field_tag :comment, params[:comment], class: 'form-control' %>
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
71
91
|
<div class="form-group d-flex justify-content-end pb-3">
|
|
72
92
|
<%= render "kaui/components/button/button", {
|
|
73
93
|
label: 'Close',
|
|
@@ -95,10 +115,19 @@
|
|
|
95
115
|
document.addEventListener('DOMContentLoaded', function () {
|
|
96
116
|
var form = document.getElementById('record_usage_form');
|
|
97
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
|
+
|
|
98
126
|
form.addEventListener('submit', function (e) {
|
|
99
127
|
var errors = [];
|
|
100
128
|
|
|
101
|
-
|
|
129
|
+
var unitTypeEl = document.getElementById('unit_type');
|
|
130
|
+
var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
|
|
102
131
|
if (unitType.length === 0) {
|
|
103
132
|
errors.push('Unit type is required.');
|
|
104
133
|
}
|
|
@@ -109,12 +138,29 @@
|
|
|
109
138
|
errors.push('Amount must be a positive integer.');
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
164
|
}
|
|
119
165
|
|
|
120
166
|
if (errors.length > 0) {
|
data/config/locales/en.yml
CHANGED
|
@@ -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
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.
|
|
4
|
+
version: 4.0.20
|
|
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-
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|