kaui 4.0.18 → 4.0.19
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/subscriptions_controller.rb +122 -31
- 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 +137 -3
- data/app/views/kaui/subscriptions/_form.html.erb +137 -3
- data/app/views/kaui/subscriptions/record_usage.erb +14 -30
- 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: f667be5cb34e03c3d1b4da18ef422934d9794643606c998cf1ac84f2e6d99f60
|
|
4
|
+
data.tar.gz: e202ccb38314d766896d3b72235de05c2818b74b985cb8b2717ff4850ab0b4ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18c8742fd7498cab0431aa9758b7f7ebda0e225d7fba76c9dd8a1fa97bd52a8678812b3a6e0397e1d15fc8128e59f451ee455ac7ebdfe8cb13de888a86101501
|
|
7
|
+
data.tar.gz: ee27c185159e1b564e0a0d16322178ddf3fbbce4c78929a0dfd3747c4c198084f1269f3e7d261047b0a2589b5f08d70be3ab07f8d727dfcc969e372929f7395a
|
|
@@ -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,119 @@ 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.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?
|
|
362
368
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
override = KillBillClient::Model::PhasePriceAttributes.new
|
|
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
|
|
369
|
+
phase = phase_meta[phase_type]
|
|
370
|
+
next if phase.nil?
|
|
369
371
|
|
|
370
|
-
|
|
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
|
|
371
384
|
|
|
372
|
-
|
|
385
|
+
def build_plan_phases_map(plans_details)
|
|
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
|
|
399
|
+
[pd.plan, phases]
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def phase_uses_fixed_price?(phase)
|
|
404
|
+
(phase.prices || []).empty?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def fetch_unit_types_from_subscription(subscription)
|
|
408
|
+
unit_types = []
|
|
409
|
+
(subscription.prices || []).each do |phase_price|
|
|
410
|
+
usage_prices = if phase_price.is_a?(Hash)
|
|
411
|
+
phase_price['usagePrices'] || phase_price[:usagePrices] || []
|
|
412
|
+
elsif phase_price.respond_to?(:usage_prices)
|
|
413
|
+
phase_price.usage_prices || []
|
|
414
|
+
else
|
|
415
|
+
[]
|
|
416
|
+
end
|
|
417
|
+
(usage_prices || []).each do |usage_price|
|
|
418
|
+
tier_prices = if usage_price.is_a?(Hash)
|
|
419
|
+
usage_price['tierPrices'] || usage_price[:tierPrices] || []
|
|
420
|
+
elsif usage_price.respond_to?(:tier_prices)
|
|
421
|
+
usage_price.tier_prices || []
|
|
422
|
+
else
|
|
423
|
+
[]
|
|
424
|
+
end
|
|
425
|
+
(tier_prices || []).each do |tier_price|
|
|
426
|
+
block_prices = if tier_price.is_a?(Hash)
|
|
427
|
+
tier_price['blockPrices'] || tier_price[:blockPrices] || []
|
|
428
|
+
elsif tier_price.respond_to?(:block_prices)
|
|
429
|
+
tier_price.block_prices || []
|
|
430
|
+
else
|
|
431
|
+
[]
|
|
432
|
+
end
|
|
433
|
+
(block_prices || []).each do |block_price|
|
|
434
|
+
unit_name = if block_price.is_a?(Hash)
|
|
435
|
+
block_price['unitName'] || block_price[:unitName] || block_price['unit_name']
|
|
436
|
+
elsif block_price.respond_to?(:unit_name)
|
|
437
|
+
block_price.unit_name
|
|
438
|
+
end
|
|
439
|
+
unit_types << unit_name if unit_name.present?
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
unit_types.uniq
|
|
445
|
+
rescue StandardError => e
|
|
446
|
+
Rails.logger.warn("Failed to extract unit types from subscription #{subscription&.subscription_id}: #{e.class}: #{e.message}")
|
|
447
|
+
[]
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def parse_usage_date(str)
|
|
451
|
+
str = str.to_s.strip
|
|
452
|
+
return nil if str.empty?
|
|
453
|
+
|
|
454
|
+
if str.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
455
|
+
year, month, day = str.split('-').map(&:to_i)
|
|
456
|
+
Time.utc(year, month, day)
|
|
457
|
+
elsif str.match?(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/)
|
|
458
|
+
Time.iso8601("#{str}:00Z")
|
|
459
|
+
else
|
|
460
|
+
Time.iso8601(str)
|
|
461
|
+
end
|
|
462
|
+
rescue ArgumentError
|
|
463
|
+
nil
|
|
373
464
|
end
|
|
374
465
|
end
|
|
375
466
|
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 d-flex 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,99 @@ $(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
|
+
return planPhases[$planSelect.val()] || [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function phaseLabel(p) {
|
|
168
|
+
var title = p.type.charAt(0) + p.type.slice(1).toLowerCase();
|
|
169
|
+
return p.priceLabel ? title + ' (' + p.priceLabel + ')' : title;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectedTypes($exceptRow) {
|
|
173
|
+
return $rows.find('.phase-override-row').filter(function() {
|
|
174
|
+
return !$exceptRow || this !== $exceptRow[0];
|
|
175
|
+
}).map(function() { return $(this).find('.phase-override-type').val(); }).get();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function refreshAddBtn() {
|
|
179
|
+
var phases = currentPhases();
|
|
180
|
+
var used = selectedTypes();
|
|
181
|
+
var hasMore = phases.length > 0 && used.length < phases.length;
|
|
182
|
+
$addBtn.toggle(hasMore);
|
|
183
|
+
$headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function rebuildRowOptions() {
|
|
187
|
+
var phases = currentPhases();
|
|
188
|
+
$rows.find('.phase-override-row').each(function() {
|
|
189
|
+
var $row = $(this);
|
|
190
|
+
var $select = $row.find('.phase-override-type');
|
|
191
|
+
var current = $select.val();
|
|
192
|
+
var used = selectedTypes($row);
|
|
193
|
+
$select.empty();
|
|
194
|
+
phases.forEach(function(p) {
|
|
195
|
+
if (used.indexOf(p.type) === -1) {
|
|
196
|
+
$select.append($('<option>', { value: p.type, text: phaseLabel(p) }));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
var currentStillValid = phases.some(function(p) { return p.type === current; }) && used.indexOf(current) === -1;
|
|
200
|
+
if (currentStillValid) $select.val(current);
|
|
201
|
+
});
|
|
202
|
+
refreshAddBtn();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function addRow(preferredType) {
|
|
206
|
+
var phases = currentPhases();
|
|
207
|
+
if (phases.length === 0) return;
|
|
208
|
+
var used = selectedTypes();
|
|
209
|
+
var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
|
|
210
|
+
? preferredType
|
|
211
|
+
: (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
|
|
212
|
+
if (!next) return;
|
|
213
|
+
|
|
214
|
+
var $row = $($template.html());
|
|
215
|
+
$rows.append($row);
|
|
216
|
+
rebuildRowOptions();
|
|
217
|
+
$row.find('.phase-override-type').val(next);
|
|
218
|
+
rebuildRowOptions();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resetRows() {
|
|
222
|
+
$rows.empty();
|
|
223
|
+
var phases = currentPhases();
|
|
224
|
+
if (phases.length > 0) addRow(phases[0].type);
|
|
225
|
+
refreshAddBtn();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
$('#toggle_phase_overrides').on('click', function() {
|
|
229
|
+
$section.toggle();
|
|
230
|
+
$('#toggle_phase_overrides_caret').html($section.is(':visible') ? '▾' : '▸');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
$addBtn.on('click', function() { addRow(); });
|
|
234
|
+
|
|
235
|
+
$rows.on('change', '.phase-override-type', rebuildRowOptions);
|
|
236
|
+
$rows.on('click', '.phase-override-remove', function() {
|
|
237
|
+
$(this).closest('.phase-override-row').remove();
|
|
238
|
+
refreshAddBtn();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
$planSelect.on('change', resetRows);
|
|
242
|
+
resetRows();
|
|
243
|
+
}
|
|
110
244
|
<% 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>Phase Overrides</strong>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="col-sm-9" id="phase_overrides_section">
|
|
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 d-flex 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,99 @@ $(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
|
+
return planPhases[$planSelect.val()] || [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function phaseLabel(p) {
|
|
185
|
+
var title = p.type.charAt(0) + p.type.slice(1).toLowerCase();
|
|
186
|
+
return p.priceLabel ? title + ' (' + p.priceLabel + ')' : title;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function selectedTypes($exceptRow) {
|
|
190
|
+
return $rows.find('.phase-override-row').filter(function() {
|
|
191
|
+
return !$exceptRow || this !== $exceptRow[0];
|
|
192
|
+
}).map(function() { return $(this).find('.phase-override-type').val(); }).get();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function refreshAddBtn() {
|
|
196
|
+
var phases = currentPhases();
|
|
197
|
+
var used = selectedTypes();
|
|
198
|
+
var hasMore = phases.length > 0 && used.length < phases.length;
|
|
199
|
+
$addBtn.toggle(hasMore);
|
|
200
|
+
$headers.css('display', $rows.find('.phase-override-row').length > 0 ? 'flex' : 'none');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function rebuildRowOptions() {
|
|
204
|
+
var phases = currentPhases();
|
|
205
|
+
$rows.find('.phase-override-row').each(function() {
|
|
206
|
+
var $row = $(this);
|
|
207
|
+
var $select = $row.find('.phase-override-type');
|
|
208
|
+
var current = $select.val();
|
|
209
|
+
var used = selectedTypes($row);
|
|
210
|
+
$select.empty();
|
|
211
|
+
phases.forEach(function(p) {
|
|
212
|
+
if (used.indexOf(p.type) === -1) {
|
|
213
|
+
$select.append($('<option>', { value: p.type, text: phaseLabel(p) }));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
var currentStillValid = phases.some(function(p) { return p.type === current; }) && used.indexOf(current) === -1;
|
|
217
|
+
if (currentStillValid) $select.val(current);
|
|
218
|
+
});
|
|
219
|
+
refreshAddBtn();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function addRow(preferredType) {
|
|
223
|
+
var phases = currentPhases();
|
|
224
|
+
if (phases.length === 0) return;
|
|
225
|
+
var used = selectedTypes();
|
|
226
|
+
var next = preferredType && phases.some(function(p) { return p.type === preferredType; }) && used.indexOf(preferredType) === -1
|
|
227
|
+
? preferredType
|
|
228
|
+
: (phases.find(function(p) { return used.indexOf(p.type) === -1; }) || {}).type;
|
|
229
|
+
if (!next) return;
|
|
230
|
+
|
|
231
|
+
var $row = $($template.html());
|
|
232
|
+
$rows.append($row);
|
|
233
|
+
rebuildRowOptions();
|
|
234
|
+
$row.find('.phase-override-type').val(next);
|
|
235
|
+
rebuildRowOptions();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resetRows() {
|
|
239
|
+
$rows.empty();
|
|
240
|
+
var phases = currentPhases();
|
|
241
|
+
if (phases.length > 0) addRow(phases[0].type);
|
|
242
|
+
refreshAddBtn();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
$('#toggle_phase_overrides').on('click', function() {
|
|
246
|
+
$section.toggle();
|
|
247
|
+
$('#toggle_phase_overrides_caret').html($section.is(':visible') ? '▾' : '▸');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
$addBtn.on('click', function() { addRow(); });
|
|
251
|
+
|
|
252
|
+
$rows.on('change', '.phase-override-type', rebuildRowOptions);
|
|
253
|
+
$rows.on('click', '.phase-override-remove', function() {
|
|
254
|
+
$(this).closest('.phase-override-row').remove();
|
|
255
|
+
refreshAddBtn();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
$planSelect.on('change', resetRows);
|
|
259
|
+
resetRows();
|
|
260
|
+
}
|
|
127
261
|
<% 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-select' %>
|
|
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,11 @@
|
|
|
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
|
-
<small class="form-text text-muted">ISO 8601 format, e.g. <code>2026-06-13T15:30:00Z</code></small>
|
|
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
52
|
</div>
|
|
48
53
|
</div>
|
|
49
54
|
|
|
@@ -54,20 +59,6 @@
|
|
|
54
59
|
</div>
|
|
55
60
|
</div>
|
|
56
61
|
|
|
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
62
|
<div class="form-group d-flex justify-content-end pb-3">
|
|
72
63
|
<%= render "kaui/components/button/button", {
|
|
73
64
|
label: 'Close',
|
|
@@ -98,7 +89,8 @@
|
|
|
98
89
|
form.addEventListener('submit', function (e) {
|
|
99
90
|
var errors = [];
|
|
100
91
|
|
|
101
|
-
var
|
|
92
|
+
var unitTypeEl = document.getElementById('unit_type');
|
|
93
|
+
var unitType = unitTypeEl ? (unitTypeEl.value || '').trim() : '';
|
|
102
94
|
if (unitType.length === 0) {
|
|
103
95
|
errors.push('Unit type is required.');
|
|
104
96
|
}
|
|
@@ -109,14 +101,6 @@
|
|
|
109
101
|
errors.push('Amount must be a positive integer.');
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
var dateStr = (document.getElementById('record_date').value || '').trim();
|
|
113
|
-
// Permissive ISO 8601 check
|
|
114
|
-
var isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
115
|
-
var parsed = Date.parse(dateStr);
|
|
116
|
-
if (dateStr.length === 0 || isNaN(parsed) || !isoRegex.test(dateStr)) {
|
|
117
|
-
errors.push('Date/time of usage must be a valid ISO 8601 timestamp (e.g. 2026-06-13T15:30:00Z).');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
104
|
if (errors.length > 0) {
|
|
121
105
|
e.preventDefault();
|
|
122
106
|
alert(errors.join('\n'));
|
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.19
|
|
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-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actionpack
|