calc_kit 0.1.0
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 +7 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/Rakefile +12 -0
- data/calc_kit.gemspec +37 -0
- data/lib/calc_kit/base.rb +61 -0
- data/lib/calc_kit/configuration.rb +21 -0
- data/lib/calc_kit/dsl.rb +99 -0
- data/lib/calc_kit/engine.rb +36 -0
- data/lib/calc_kit/input_definition.rb +63 -0
- data/lib/calc_kit/output_definition.rb +35 -0
- data/lib/calc_kit/rails/controller_helpers.rb +76 -0
- data/lib/calc_kit/rails/model.rb +48 -0
- data/lib/calc_kit/rails/view_helpers.rb +66 -0
- data/lib/calc_kit/registry.rb +37 -0
- data/lib/calc_kit/version.rb +5 -0
- data/lib/calc_kit.rb +42 -0
- data/lib/generators/calc_kit/calculator_generator.rb +39 -0
- data/lib/generators/calc_kit/install_generator.rb +68 -0
- data/lib/generators/calc_kit/templates/application_calculator.rb.tt +13 -0
- data/lib/generators/calc_kit/templates/calculation.rb.tt +9 -0
- data/lib/generators/calc_kit/templates/calculator.rb.tt +27 -0
- data/lib/generators/calc_kit/templates/calculator_test.rb.tt +32 -0
- data/lib/generators/calc_kit/templates/create_calculations.rb.tt +22 -0
- data/lib/generators/calc_kit/templates/initializer.rb.tt +27 -0
- data/lib/generators/calc_kit/templates/views/_form.html.erb +30 -0
- data/lib/generators/calc_kit/templates/views/_result.html.erb +17 -0
- data/lib/generators/calc_kit/templates/views/_saved_calculations.html.erb +36 -0
- data/lib/generators/calc_kit/templates/views/create.turbo_stream.erb +3 -0
- data/lib/generators/calc_kit/templates/views/index.html.erb +11 -0
- data/lib/generators/calc_kit/templates/views/show.html.erb +13 -0
- metadata +132 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# Controller helpers for calculator actions
|
|
5
|
+
module ControllerHelpers
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
helper_method :calc_kit_scope if respond_to?(:helper_method)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Find a calculator class by slug, raises if not found
|
|
15
|
+
def find_calculator_class(slug)
|
|
16
|
+
CalcKit.find(slug) || raise(CalcKit::NotFoundError, "Calculator not found: #{slug}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Build a calculator instance from params
|
|
20
|
+
def build_calculator(calculator_class, params_hash = nil)
|
|
21
|
+
params_hash ||= calculator_params_for(calculator_class)
|
|
22
|
+
calculator_class.new(params_hash)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Extract permitted params for a calculator
|
|
26
|
+
def calculator_params_for(calculator_class)
|
|
27
|
+
permitted = calculator_class.inputs.map(&:name)
|
|
28
|
+
params.permit(*permitted).to_h.reject { |_, v| v.blank? }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Save a calculation result
|
|
32
|
+
def save_calculation(calculator, result, calculation_class: nil)
|
|
33
|
+
return unless CalcKit.configuration.save_calculations
|
|
34
|
+
|
|
35
|
+
calculation_class ||= "Calculation".safe_constantize
|
|
36
|
+
return unless calculation_class
|
|
37
|
+
|
|
38
|
+
scope = calc_kit_scope
|
|
39
|
+
attrs = {
|
|
40
|
+
calculator_type: calculator.class.calculator_slug.to_s,
|
|
41
|
+
calculator_version: calculator.class.version,
|
|
42
|
+
inputs: calculator.input_values,
|
|
43
|
+
outputs: result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if scope
|
|
47
|
+
scope.calculations.create!(attrs)
|
|
48
|
+
else
|
|
49
|
+
calculation_class.create!(attrs)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Load saved calculations for a calculator
|
|
54
|
+
def load_saved_calculations(calculator_class, limit: 5, calculation_class: nil)
|
|
55
|
+
return [] unless CalcKit.configuration.save_calculations
|
|
56
|
+
|
|
57
|
+
calculation_class ||= "Calculation".safe_constantize
|
|
58
|
+
return [] unless calculation_class
|
|
59
|
+
|
|
60
|
+
scope = calc_kit_scope
|
|
61
|
+
relation = scope ? scope.calculations : calculation_class
|
|
62
|
+
relation
|
|
63
|
+
.where(calculator_type: calculator_class.calculator_slug.to_s)
|
|
64
|
+
.order(created_at: :desc)
|
|
65
|
+
.limit(limit)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the scope for multi-tenancy (e.g., current_account)
|
|
69
|
+
def calc_kit_scope
|
|
70
|
+
scope_method = CalcKit.configuration.scope_method
|
|
71
|
+
return nil unless scope_method
|
|
72
|
+
|
|
73
|
+
respond_to?(scope_method, true) ? send(scope_method) : nil
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# Concern for the Calculation model that stores calculation results
|
|
5
|
+
module Model
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
validates :calculator_type, presence: true
|
|
10
|
+
validates :calculator_version, presence: true
|
|
11
|
+
validates :inputs, presence: true
|
|
12
|
+
validates :outputs, presence: true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns the calculator class for this calculation
|
|
16
|
+
def calculator_class
|
|
17
|
+
CalcKit.find(calculator_type)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns a new calculator instance populated with the saved inputs
|
|
21
|
+
def to_calculator
|
|
22
|
+
klass = calculator_class
|
|
23
|
+
return nil unless klass
|
|
24
|
+
|
|
25
|
+
klass.new(inputs.symbolize_keys)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if the calculator version matches the current version
|
|
29
|
+
def version_current?
|
|
30
|
+
klass = calculator_class
|
|
31
|
+
return false unless klass
|
|
32
|
+
|
|
33
|
+
klass.version == calculator_version
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if there's a version mismatch
|
|
37
|
+
def version_mismatch?
|
|
38
|
+
!version_current?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
module ClassMethods
|
|
42
|
+
# Scope to filter by calculator type
|
|
43
|
+
def for_calculator(slug)
|
|
44
|
+
where(calculator_type: slug.to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# View helpers for rendering calculator forms and results
|
|
5
|
+
module ViewHelpers
|
|
6
|
+
# Resolve a default value, calling it if it's a proc
|
|
7
|
+
def calc_kit_resolve_default(default)
|
|
8
|
+
default.respond_to?(:call) ? default.call : default
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Format an output value based on its type
|
|
12
|
+
def calc_kit_format_output(value, type)
|
|
13
|
+
return "" if value.nil?
|
|
14
|
+
|
|
15
|
+
case type
|
|
16
|
+
when :date
|
|
17
|
+
value&.respond_to?(:to_fs) ? value.to_fs(:long) : value.to_s
|
|
18
|
+
when :decimal
|
|
19
|
+
if respond_to?(:number_with_precision)
|
|
20
|
+
number_with_precision(value, precision: 2)
|
|
21
|
+
else
|
|
22
|
+
format("%.2f", value.to_f)
|
|
23
|
+
end
|
|
24
|
+
when :integer
|
|
25
|
+
value.to_i.to_s
|
|
26
|
+
when :currency
|
|
27
|
+
if respond_to?(:number_to_currency)
|
|
28
|
+
number_to_currency(value)
|
|
29
|
+
else
|
|
30
|
+
format("$%.2f", value.to_f)
|
|
31
|
+
end
|
|
32
|
+
when :percentage
|
|
33
|
+
if respond_to?(:number_to_percentage)
|
|
34
|
+
number_to_percentage(value, precision: 1)
|
|
35
|
+
else
|
|
36
|
+
format("%.1f%%", value.to_f)
|
|
37
|
+
end
|
|
38
|
+
else
|
|
39
|
+
value.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
KEYSTONE_TYPE_MAP = {
|
|
44
|
+
string: :text,
|
|
45
|
+
integer: :number,
|
|
46
|
+
decimal: :number,
|
|
47
|
+
date: :date,
|
|
48
|
+
select: :select,
|
|
49
|
+
boolean: :checkbox
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# Map CalcKit input type to keystone_ui form field type
|
|
53
|
+
def calc_kit_keystone_type(type)
|
|
54
|
+
KEYSTONE_TYPE_MAP.fetch(type, :text)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns a warning string if the calculation was made with a different
|
|
58
|
+
# calculator version, or nil if versions match or warnings are disabled.
|
|
59
|
+
def calc_kit_version_warning(calculation)
|
|
60
|
+
return nil unless CalcKit.configuration.warn_on_version_mismatch
|
|
61
|
+
return nil unless calculation.version_mismatch?
|
|
62
|
+
|
|
63
|
+
"This calculation was made with a different version of the calculator and may have outdated results."
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CalcKit
|
|
4
|
+
# Registry for calculator lookup by slug
|
|
5
|
+
class Registry
|
|
6
|
+
def initialize
|
|
7
|
+
@calculators = {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def register(calculator_class)
|
|
11
|
+
slug = calculator_class.calculator_slug
|
|
12
|
+
raise ArgumentError, "Calculator must define a slug" unless slug
|
|
13
|
+
|
|
14
|
+
@calculators[slug.to_sym] = calculator_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def find(slug)
|
|
18
|
+
@calculators[slug.to_sym]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all
|
|
22
|
+
@calculators.values
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def slugs
|
|
26
|
+
@calculators.keys
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clear
|
|
30
|
+
@calculators.clear
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def registered?(slug)
|
|
34
|
+
@calculators.key?(slug.to_sym)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/calc_kit.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "calc_kit/version"
|
|
4
|
+
require_relative "calc_kit/configuration"
|
|
5
|
+
require_relative "calc_kit/input_definition"
|
|
6
|
+
require_relative "calc_kit/output_definition"
|
|
7
|
+
require_relative "calc_kit/dsl"
|
|
8
|
+
require_relative "calc_kit/registry"
|
|
9
|
+
require_relative "calc_kit/base"
|
|
10
|
+
|
|
11
|
+
module CalcKit
|
|
12
|
+
class NotFoundError < StandardError; end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure
|
|
20
|
+
yield(configuration)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def registry
|
|
24
|
+
@registry ||= Registry.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def register(calculator_class)
|
|
28
|
+
registry.register(calculator_class)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def find(slug)
|
|
32
|
+
registry.find(slug)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all
|
|
36
|
+
registry.all
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Load Rails engine if Rails is present
|
|
42
|
+
require_relative "calc_kit/engine" if defined?(Rails::Engine)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/base"
|
|
5
|
+
|
|
6
|
+
module CalcKit
|
|
7
|
+
module Generators
|
|
8
|
+
class CalculatorGenerator < Rails::Generators::NamedBase
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :skip_test, type: :boolean, default: false,
|
|
12
|
+
desc: "Skip generating test file"
|
|
13
|
+
|
|
14
|
+
def create_calculator
|
|
15
|
+
template "calculator.rb.tt", "app/calculators/#{file_name}_calculator.rb"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def create_test
|
|
19
|
+
return if options[:skip_test]
|
|
20
|
+
|
|
21
|
+
template "calculator_test.rb.tt", "test/calculators/#{file_name}_calculator_test.rb"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def class_name
|
|
27
|
+
file_name.camelize
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def calculator_name
|
|
31
|
+
file_name.titleize
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def calculator_slug
|
|
35
|
+
file_name.underscore
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/base"
|
|
5
|
+
|
|
6
|
+
module CalcKit
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
class_option :model, type: :boolean, default: false,
|
|
12
|
+
desc: "Generate Calculation model and migration"
|
|
13
|
+
class_option :views, type: :boolean, default: false,
|
|
14
|
+
desc: "Copy default views to app/views/calculators"
|
|
15
|
+
class_option :scope, type: :string, default: nil,
|
|
16
|
+
desc: "Multi-tenancy scope method (e.g., current_account)"
|
|
17
|
+
|
|
18
|
+
def create_initializer
|
|
19
|
+
template "initializer.rb.tt", "config/initializers/calc_kit.rb"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_calculators_directory
|
|
23
|
+
empty_directory "app/calculators"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create_application_calculator
|
|
27
|
+
template "application_calculator.rb.tt", "app/calculators/application_calculator.rb"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_model
|
|
31
|
+
return unless options[:model]
|
|
32
|
+
|
|
33
|
+
template "calculation.rb.tt", "app/models/calculation.rb"
|
|
34
|
+
migration_template "create_calculations.rb.tt",
|
|
35
|
+
"db/migrate/create_calculations.rb",
|
|
36
|
+
migration_version: migration_version
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def copy_views
|
|
40
|
+
return unless options[:views]
|
|
41
|
+
|
|
42
|
+
directory "views", "app/views/calculators"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show_post_install_message
|
|
46
|
+
say ""
|
|
47
|
+
say "CalcKit installed successfully!", :green
|
|
48
|
+
say ""
|
|
49
|
+
say "Next steps:"
|
|
50
|
+
say " 1. Run `rails db:migrate` if you generated the model"
|
|
51
|
+
say " 2. Create a calculator: `rails g calc_kit:calculator my_calculator`"
|
|
52
|
+
say " 3. Define inputs, outputs, and the calculate method"
|
|
53
|
+
say " 4. Register your calculator at the bottom of the file"
|
|
54
|
+
say ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def migration_version
|
|
60
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scope_method
|
|
64
|
+
options[:scope]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Base class for all calculators in this application.
|
|
4
|
+
# Inherits from CalcKit::Base which provides the DSL.
|
|
5
|
+
#
|
|
6
|
+
# Add shared behavior, validations, or methods here.
|
|
7
|
+
#
|
|
8
|
+
class ApplicationCalculator < CalcKit::Base
|
|
9
|
+
# Example shared method:
|
|
10
|
+
# def formatted_currency(amount)
|
|
11
|
+
# "$%.2f" % amount
|
|
12
|
+
# end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>Calculator < ApplicationCalculator
|
|
4
|
+
calculator_name "<%= calculator_name %>"
|
|
5
|
+
calculator_slug :<%= calculator_slug %>
|
|
6
|
+
version "1.0"
|
|
7
|
+
|
|
8
|
+
# Define inputs
|
|
9
|
+
# input :price, :decimal, label: "Price", required: true, min: 0.01
|
|
10
|
+
# input :quantity, :integer, label: "Quantity", required: true, min: 1
|
|
11
|
+
|
|
12
|
+
# Define outputs
|
|
13
|
+
# output :total, :decimal
|
|
14
|
+
# output :summary, :string
|
|
15
|
+
|
|
16
|
+
def calculate
|
|
17
|
+
# Return a hash with output values
|
|
18
|
+
# {
|
|
19
|
+
# total: price * quantity,
|
|
20
|
+
# summary: "#{quantity} items at $#{price} each"
|
|
21
|
+
# }
|
|
22
|
+
raise NotImplementedError, "Implement the calculate method"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Register the calculator
|
|
27
|
+
CalcKit.register(<%= class_name %>Calculator)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class <%= class_name %>CalculatorTest < ActiveSupport::TestCase
|
|
6
|
+
def setup
|
|
7
|
+
@calculator = <%= class_name %>Calculator.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
test "calculator is registered" do
|
|
11
|
+
assert_equal <%= class_name %>Calculator, CalcKit.find(:<%= calculator_slug %>)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test "calculator has required metadata" do
|
|
15
|
+
assert_equal "<%= calculator_name %>", <%= class_name %>Calculator.calculator_name
|
|
16
|
+
assert_equal :<%= calculator_slug %>, <%= class_name %>Calculator.calculator_slug
|
|
17
|
+
assert_equal "1.0", <%= class_name %>Calculator.version
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# test "validates required inputs" do
|
|
21
|
+
# assert_not @calculator.valid?
|
|
22
|
+
# assert_includes @calculator.errors[:price], "can't be blank"
|
|
23
|
+
# end
|
|
24
|
+
|
|
25
|
+
# test "calculates correctly with valid inputs" do
|
|
26
|
+
# calculator = <%= class_name %>Calculator.new(price: 10.0, quantity: 5)
|
|
27
|
+
# result = calculator.run
|
|
28
|
+
#
|
|
29
|
+
# assert_not_nil result
|
|
30
|
+
# assert_equal 50.0, result[:total]
|
|
31
|
+
# end
|
|
32
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateCalculations < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
+
def change
|
|
5
|
+
create_table :calculations do |t|
|
|
6
|
+
<% if scope_method -%>
|
|
7
|
+
t.references :<%= scope_method.to_s.sub("current_", "") %>, null: true, foreign_key: true
|
|
8
|
+
<% end -%>
|
|
9
|
+
t.string :calculator_type, null: false
|
|
10
|
+
t.string :calculator_version, null: false
|
|
11
|
+
t.jsonb :inputs, null: false, default: {}
|
|
12
|
+
t.jsonb :outputs, null: false, default: {}
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :calculations, :calculator_type
|
|
18
|
+
<% if scope_method -%>
|
|
19
|
+
add_index :calculations, [:<%= scope_method.to_s.sub("current_", "") %>_id, :calculator_type]
|
|
20
|
+
<% end -%>
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
CalcKit.configure do |config|
|
|
4
|
+
# Path to calculator classes (relative to Rails.root)
|
|
5
|
+
# config.calculators_path = "app/calculators"
|
|
6
|
+
|
|
7
|
+
# Automatically register calculators on load
|
|
8
|
+
# config.auto_register = true
|
|
9
|
+
<% if scope_method -%>
|
|
10
|
+
|
|
11
|
+
# Multi-tenancy scope method
|
|
12
|
+
config.scope_method = :<%= scope_method %>
|
|
13
|
+
<% else -%>
|
|
14
|
+
|
|
15
|
+
# Multi-tenancy scope method (e.g., :current_account, :current_user)
|
|
16
|
+
# config.scope_method = nil
|
|
17
|
+
<% end -%>
|
|
18
|
+
|
|
19
|
+
# Enable Turbo Streams for form responses
|
|
20
|
+
# config.enable_turbo_streams = true
|
|
21
|
+
|
|
22
|
+
# Enable saving calculations to the database
|
|
23
|
+
# config.save_calculations = true
|
|
24
|
+
|
|
25
|
+
# Warn when loading a calculation with a different version
|
|
26
|
+
# config.warn_on_version_mismatch = true
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%%= ui_form action: calculator_path(slug: calculator.class.calculator_slug),
|
|
2
|
+
method: :post,
|
|
3
|
+
data: { turbo_stream: true } do %>
|
|
4
|
+
|
|
5
|
+
<%% calculator.class.inputs.each do |input| %>
|
|
6
|
+
<%%= ui_form_field attribute: input.name,
|
|
7
|
+
label: input.label,
|
|
8
|
+
type: calc_kit_keystone_type(input.type),
|
|
9
|
+
required: input.required?,
|
|
10
|
+
hint: input.hint,
|
|
11
|
+
placeholder: input.placeholder,
|
|
12
|
+
min: input.min,
|
|
13
|
+
max: input.max,
|
|
14
|
+
step: input.type == :decimal ? (input.step || "0.01") : (input.type == :integer ? 1 : nil),
|
|
15
|
+
value: calculator.send(input.name) || calc_kit_resolve_default(input.default),
|
|
16
|
+
options: input.respond_to?(:options_for_select) ? input.options_for_select : [],
|
|
17
|
+
errors: calculator.errors[input.name].to_a %>
|
|
18
|
+
<%% end %>
|
|
19
|
+
|
|
20
|
+
<div class="flex items-center gap-4">
|
|
21
|
+
<%%= ui_button label: "Calculate", type: :submit %>
|
|
22
|
+
|
|
23
|
+
<%% if defined?(user_signed_in?) && user_signed_in? %>
|
|
24
|
+
<label class="inline-flex items-center">
|
|
25
|
+
<%%= check_box_tag :save, "1", false, class: "form-checkbox rounded" %>
|
|
26
|
+
<span class="ml-2 text-sm">Save to my calculations</span>
|
|
27
|
+
</label>
|
|
28
|
+
<%% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<%% end %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<div id="calculator_result">
|
|
2
|
+
<%% if result %>
|
|
3
|
+
<div class="mt-6">
|
|
4
|
+
<%%= ui_panel do %>
|
|
5
|
+
<h3 class="text-lg font-semibold mb-4">Result</h3>
|
|
6
|
+
<dl class="space-y-2">
|
|
7
|
+
<%% calculator.class.outputs.each do |output| %>
|
|
8
|
+
<div class="flex justify-between">
|
|
9
|
+
<dt class="font-medium"><%%= output.label %>:</dt>
|
|
10
|
+
<dd><%%= calc_kit_format_output(result[output.name], output.type) %></dd>
|
|
11
|
+
</div>
|
|
12
|
+
<%% end %>
|
|
13
|
+
</dl>
|
|
14
|
+
<%% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<%% end %>
|
|
17
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%% if saved_calculations&.any? %>
|
|
2
|
+
<div class="mt-6">
|
|
3
|
+
<h3 class="text-sm font-semibold text-gray-500 mb-3">Recent Calculations</h3>
|
|
4
|
+
<div class="space-y-2">
|
|
5
|
+
<%% saved_calculations.each do |calculation| %>
|
|
6
|
+
<%%= ui_panel padding: :sm do %>
|
|
7
|
+
<div class="flex justify-between items-start gap-4 text-sm">
|
|
8
|
+
<div class="flex-1 min-w-0">
|
|
9
|
+
<div class="text-xs text-gray-400 mb-1">
|
|
10
|
+
<%%= calculation.created_at.to_fs(:short) %>
|
|
11
|
+
<%% if (version_warning = calc_kit_version_warning(calculation)) %>
|
|
12
|
+
<%%= ui_badge label: "Outdated", variant: :warning %>
|
|
13
|
+
<%% end %>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex flex-wrap gap-x-4 gap-y-1">
|
|
16
|
+
<%% calculation.outputs.each do |key, value| %>
|
|
17
|
+
<span class="text-gray-700 dark:text-gray-300">
|
|
18
|
+
<span class="text-gray-500"><%%= key.to_s.titleize %>:</span> <%%= value %>
|
|
19
|
+
</span>
|
|
20
|
+
<%% end %>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<%% if defined?(reopen_calculation_path) %>
|
|
24
|
+
<div class="flex gap-2 shrink-0">
|
|
25
|
+
<%%= ui_button label: "Use", href: reopen_calculation_path(calculation), variant: :secondary, size: :sm %>
|
|
26
|
+
</div>
|
|
27
|
+
<%% end %>
|
|
28
|
+
</div>
|
|
29
|
+
<%% end %>
|
|
30
|
+
<%% end %>
|
|
31
|
+
</div>
|
|
32
|
+
<%% if defined?(calculations_path) %>
|
|
33
|
+
<%%= ui_button label: "View all", href: calculations_path, variant: :secondary, size: :sm %>
|
|
34
|
+
<%% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<%% end %>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%%= ui_page max_width: :lg do %>
|
|
2
|
+
<%%= ui_page_header title: "Calculators" %>
|
|
3
|
+
|
|
4
|
+
<%%= ui_grid cols: { default: 1, md: 2, lg: 3 } do %>
|
|
5
|
+
<%% CalcKit.all.each do |calculator_class| %>
|
|
6
|
+
<%%= ui_card_link href: calculator_path(slug: calculator_class.calculator_slug) do %>
|
|
7
|
+
<h2 class="text-lg font-semibold"><%%= calculator_class.calculator_name %></h2>
|
|
8
|
+
<%% end %>
|
|
9
|
+
<%% end %>
|
|
10
|
+
<%% end %>
|
|
11
|
+
<%% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%%= ui_page max_width: :lg do %>
|
|
2
|
+
<%%= ui_page_header title: @calculator_class.calculator_name %>
|
|
3
|
+
|
|
4
|
+
<%%= ui_panel do %>
|
|
5
|
+
<%%= render "form", calculator: @calculator %>
|
|
6
|
+
<%% end %>
|
|
7
|
+
|
|
8
|
+
<%%= render "result", calculator: @calculator, result: @result %>
|
|
9
|
+
|
|
10
|
+
<%% if defined?(@saved_calculations) && @saved_calculations&.any? %>
|
|
11
|
+
<%%= render "saved_calculations", saved_calculations: @saved_calculations %>
|
|
12
|
+
<%% end %>
|
|
13
|
+
<%% end %>
|