nti_receipt_builder 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 +21 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/nti_receipt_builder/index.js +16 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_builder_controller.js +297 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_canvas_controller.js +279 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_draggable_controller.js +154 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_print_controller.js +24 -0
- data/app/assets/javascripts/nti_receipt_builder/receipt_properties_controller.js +297 -0
- data/app/assets/stylesheets/nti_receipt_builder.css +238 -0
- data/app/controllers/concerns/nti_receipt_builder/templates_controller.rb +170 -0
- data/app/helpers/nti_receipt_builder/render_helper.rb +21 -0
- data/app/models/nti_receipt_builder/template.rb +79 -0
- data/app/views/nti_receipt_builder/render/_order_lines_element.html.erb +36 -0
- data/app/views/nti_receipt_builder/render/_overflow_banner.html.erb +5 -0
- data/app/views/nti_receipt_builder/render/_paper.html.erb +12 -0
- data/app/views/nti_receipt_builder/render/_text_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/render/_variable_element.html.erb +1 -0
- data/app/views/nti_receipt_builder/templates/_canvas.html.erb +33 -0
- data/app/views/nti_receipt_builder/templates/_elements_panel.html.erb +49 -0
- data/app/views/nti_receipt_builder/templates/_errors.html.erb +21 -0
- data/app/views/nti_receipt_builder/templates/_lock_version_field.html.erb +2 -0
- data/app/views/nti_receipt_builder/templates/_print_assets.html.erb +15 -0
- data/app/views/nti_receipt_builder/templates/_properties_panel.html.erb +8 -0
- data/app/views/nti_receipt_builder/templates/_settings_panel.html.erb +130 -0
- data/app/views/nti_receipt_builder/templates/create.turbo_stream.erb +3 -0
- data/app/views/nti_receipt_builder/templates/edit.html.erb +54 -0
- data/app/views/nti_receipt_builder/templates/new.html.erb +115 -0
- data/app/views/nti_receipt_builder/templates/preview.html.erb +24 -0
- data/app/views/nti_receipt_builder/templates/print.html.erb +31 -0
- data/app/views/nti_receipt_builder/templates/render_preview.turbo_stream.erb +26 -0
- data/app/views/nti_receipt_builder/templates/update.turbo_stream.erb +7 -0
- data/config/importmap.rb +4 -0
- data/lib/generators/nti_receipt_builder/install_generator.rb +49 -0
- data/lib/generators/nti_receipt_builder/templates/create_nti_receipt_templates.rb +28 -0
- data/lib/generators/nti_receipt_builder/templates/initializer.rb +16 -0
- data/lib/generators/nti_receipt_builder/templates/turbo_stream_tags.rb +36 -0
- data/lib/nti_receipt_builder/configuration.rb +59 -0
- data/lib/nti_receipt_builder/elements.rb +46 -0
- data/lib/nti_receipt_builder/engine.rb +32 -0
- data/lib/nti_receipt_builder/errors.rb +13 -0
- data/lib/nti_receipt_builder/layout_validator.rb +285 -0
- data/lib/nti_receipt_builder/presenters/collection.rb +126 -0
- data/lib/nti_receipt_builder/presenters/element.rb +68 -0
- data/lib/nti_receipt_builder/render_result.rb +18 -0
- data/lib/nti_receipt_builder/renderer.rb +78 -0
- data/lib/nti_receipt_builder/set_default.rb +24 -0
- data/lib/nti_receipt_builder/template_form.rb +60 -0
- data/lib/nti_receipt_builder/type_inference.rb +26 -0
- data/lib/nti_receipt_builder/variable_definition.rb +74 -0
- data/lib/nti_receipt_builder/variable_resolver.rb +55 -0
- data/lib/nti_receipt_builder/variables.rb +159 -0
- data/lib/nti_receipt_builder/version.rb +5 -0
- data/lib/nti_receipt_builder.rb +50 -0
- metadata +175 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiReceiptBuilder
|
|
4
|
+
# The builder's actions, for inclusion in a host controller that owns routing,
|
|
5
|
+
# authorization and tenancy.
|
|
6
|
+
#
|
|
7
|
+
# class Partner::ReceiptTemplatesController < PartnerController
|
|
8
|
+
# include NtiReceiptBuilder::TemplatesController
|
|
9
|
+
#
|
|
10
|
+
# private
|
|
11
|
+
#
|
|
12
|
+
# def receipt_owner = current_account
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Every query runs through `receipt_owner`, so the tenant boundary stays visible in host
|
|
16
|
+
# code rather than in a configured lambda. Hosts may override any action or navigation
|
|
17
|
+
# hook — a method defined in the including class wins over the one mixed in here.
|
|
18
|
+
module TemplatesController
|
|
19
|
+
extend ActiveSupport::Concern
|
|
20
|
+
|
|
21
|
+
VIEW_PREFIX = 'nti_receipt_builder/templates'
|
|
22
|
+
|
|
23
|
+
included do
|
|
24
|
+
before_action :find_receipt_template,
|
|
25
|
+
only: %i[edit update destroy preview render_preview print set_default]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Append the gem's view prefix so bare `render :edit` resolves to the engine's templates.
|
|
29
|
+
# The host's own prefixes stay first, so dropping a receipt_templates/edit.html.erb into the
|
|
30
|
+
# host shadows the gem's copy without any configuration.
|
|
31
|
+
def _prefixes
|
|
32
|
+
@_prefixes ||= super + [VIEW_PREFIX]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def new
|
|
36
|
+
@receipt_template = receipt_templates.new(default_template_attributes)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create
|
|
40
|
+
form = TemplateForm.new(receipt_template_params)
|
|
41
|
+
|
|
42
|
+
unless form.valid?
|
|
43
|
+
@errors = form.errors.full_messages
|
|
44
|
+
@receipt_template = receipt_templates.new(default_template_attributes)
|
|
45
|
+
return render_create_failure
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
@receipt_template = receipt_templates.new(form.attributes)
|
|
49
|
+
|
|
50
|
+
if @receipt_template.save
|
|
51
|
+
redirect_to after_create_path(@receipt_template),
|
|
52
|
+
notice: 'Receipt template created! Now build your layout.'
|
|
53
|
+
else
|
|
54
|
+
@errors = @receipt_template.errors.full_messages
|
|
55
|
+
render_create_failure
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def edit; end
|
|
60
|
+
|
|
61
|
+
def update
|
|
62
|
+
form = TemplateForm.new(receipt_template_params)
|
|
63
|
+
|
|
64
|
+
if form.valid?
|
|
65
|
+
persist_update(form)
|
|
66
|
+
else
|
|
67
|
+
@errors = form.errors.full_messages
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
render :update, status: @errors.empty? ? :ok : :unprocessable_content
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def destroy
|
|
74
|
+
@receipt_template.destroy
|
|
75
|
+
redirect_to after_destroy_path, notice: 'Receipt template deleted!'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def preview
|
|
79
|
+
@render_result = Renderer.new(template: @receipt_template).call
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_preview
|
|
83
|
+
form = TemplateForm.new(receipt_template_params)
|
|
84
|
+
|
|
85
|
+
@draft = @receipt_template.dup
|
|
86
|
+
@draft.assign_attributes(form.attributes.except(:lock_version))
|
|
87
|
+
|
|
88
|
+
if !form.valid?
|
|
89
|
+
@render_result = nil
|
|
90
|
+
@draft_errors = form.errors.full_messages
|
|
91
|
+
elsif @draft.valid?
|
|
92
|
+
@render_result = Renderer.new(template: @draft).call
|
|
93
|
+
@draft_errors = []
|
|
94
|
+
else
|
|
95
|
+
@render_result = nil
|
|
96
|
+
@draft_errors = @draft.errors.full_messages
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def print
|
|
101
|
+
@render_result = Renderer.new(template: @receipt_template).call
|
|
102
|
+
|
|
103
|
+
render :print, layout: false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_default
|
|
107
|
+
SetDefault.new(template: @receipt_template).call
|
|
108
|
+
|
|
109
|
+
redirect_to after_set_default_path, notice: 'Default receipt template updated!'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# Hosts MUST implement this. Every query is scoped through it, which is what keeps a
|
|
115
|
+
# multi-tenant host from leaking templates across tenants.
|
|
116
|
+
def receipt_owner
|
|
117
|
+
raise NotImplementedError, "#{self.class.name} must define #receipt_owner"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def receipt_templates
|
|
121
|
+
NtiReceiptBuilder.template_class.where(owner: receipt_owner)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def find_receipt_template
|
|
125
|
+
@receipt_template = receipt_templates.find_by(id: params[:id])
|
|
126
|
+
|
|
127
|
+
redirect_to after_destroy_path, alert: 'Receipt template not found' unless @receipt_template
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The new-template form submits as a Turbo Stream, which patches the error region in place;
|
|
131
|
+
# a plain HTML submit re-renders the whole form. Both carry @errors.
|
|
132
|
+
def render_create_failure
|
|
133
|
+
respond_to do |format|
|
|
134
|
+
format.turbo_stream { render :create, status: :unprocessable_content }
|
|
135
|
+
format.html { render :new, status: :unprocessable_content }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def persist_update(form)
|
|
140
|
+
@receipt_template.assign_attributes(form.attributes)
|
|
141
|
+
@errors = @receipt_template.save ? [] : @receipt_template.errors.full_messages
|
|
142
|
+
@receipt_template.reload if @errors.empty?
|
|
143
|
+
rescue ActiveRecord::StaleObjectError
|
|
144
|
+
@errors = ['Someone else has already saved changes to this template. Please reload and try again.']
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def receipt_template_params
|
|
148
|
+
params.expect(
|
|
149
|
+
receipt_template: %i[
|
|
150
|
+
name paper_size_key paper_width_mm paper_height_mm orientation
|
|
151
|
+
height_mode margin_top_mm margin_right_mm margin_bottom_mm
|
|
152
|
+
margin_left_mm layout lock_version
|
|
153
|
+
]
|
|
154
|
+
).to_h.symbolize_keys
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def default_template_attributes
|
|
158
|
+
{
|
|
159
|
+
paper_size_key: 'roll_80mm', paper_width_mm: 80.0, paper_height_mm: 200.0,
|
|
160
|
+
orientation: 'portrait', height_mode: 'content',
|
|
161
|
+
margin_top_mm: 2, margin_right_mm: 2, margin_bottom_mm: 2, margin_left_mm: 2
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Overridable navigation hooks — hosts with named routes should override these.
|
|
166
|
+
def after_create_path(template) = url_for(action: :edit, id: template.id)
|
|
167
|
+
def after_destroy_path = url_for(action: :index)
|
|
168
|
+
def after_set_default_path = url_for(action: :index)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiReceiptBuilder
|
|
4
|
+
# View helpers for the shared render partials. These are rendered from more than one host
|
|
5
|
+
# controller — the designer's preview/print and a printed receipt — so they live here rather
|
|
6
|
+
# than in a controller-scoped helper.
|
|
7
|
+
module RenderHelper
|
|
8
|
+
# The paper element's inline size: fixed to the declared dimensions, except in "content"
|
|
9
|
+
# height mode where it grows to fit whatever the renderer computed so nothing gets clipped.
|
|
10
|
+
def paper_style(receipt_template, render_result)
|
|
11
|
+
height_mm = if receipt_template.content_layout?
|
|
12
|
+
[receipt_template.paper_height_mm.to_f,
|
|
13
|
+
render_result.content_height_mm + receipt_template.margin_bottom_mm.to_f].max
|
|
14
|
+
else
|
|
15
|
+
receipt_template.paper_height_mm.to_f
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
"width: #{receipt_template.paper_width_mm.to_f}mm; height: #{height_mm}mm;"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NtiReceiptBuilder
|
|
4
|
+
# A receipt template: paper geometry plus a jsonb layout of positioned elements.
|
|
5
|
+
#
|
|
6
|
+
# Hosts may subclass this to attach their own concerns — audit trail, archivable status,
|
|
7
|
+
# tenancy — and point `config.template_class` at the subclass so the gem's own queries
|
|
8
|
+
# return host instances. `inheritance_column = nil` keeps the subclass sharing this table
|
|
9
|
+
# with no STI type condition and no `type` column to maintain.
|
|
10
|
+
class Template < ActiveRecord::Base
|
|
11
|
+
self.table_name = 'nti_receipt_templates'
|
|
12
|
+
self.inheritance_column = nil
|
|
13
|
+
|
|
14
|
+
belongs_to :owner, polymorphic: true
|
|
15
|
+
|
|
16
|
+
ORIENTATIONS = %w[portrait landscape].freeze
|
|
17
|
+
HEIGHT_MODES = %w[fixed content].freeze
|
|
18
|
+
|
|
19
|
+
MIN_PAPER_DIMENSION_MM = 10.0
|
|
20
|
+
MAX_PAPER_DIMENSION_MM = 1000.0
|
|
21
|
+
MIN_MARGIN_MM = 0.0
|
|
22
|
+
MAX_MARGIN_MM = 100.0
|
|
23
|
+
|
|
24
|
+
PAPER_PRESETS = {
|
|
25
|
+
'roll_58mm' => { label: '58mm Roll', width_mm: 58.0, height_mm: 297.0, orientation: 'portrait' },
|
|
26
|
+
'roll_80mm' => { label: '80mm Roll', width_mm: 80.0, height_mm: 297.0, orientation: 'portrait' },
|
|
27
|
+
'a4' => { label: 'A4', width_mm: 210.0, height_mm: 297.0, orientation: 'portrait' },
|
|
28
|
+
'letter' => { label: 'Letter', width_mm: 215.9, height_mm: 279.4, orientation: 'portrait' },
|
|
29
|
+
'custom' => { label: 'Custom', width_mm: 80.0, height_mm: 200.0, orientation: 'portrait' }
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
validates :name, presence: true, length: { maximum: 120 }
|
|
33
|
+
validates :paper_size_key, presence: true, inclusion: { in: PAPER_PRESETS.keys }
|
|
34
|
+
validates :orientation, presence: true, inclusion: { in: ORIENTATIONS }
|
|
35
|
+
validates :height_mode, presence: true, inclusion: { in: HEIGHT_MODES }
|
|
36
|
+
validates :paper_width_mm, :paper_height_mm,
|
|
37
|
+
presence: true,
|
|
38
|
+
numericality: { greater_than_or_equal_to: MIN_PAPER_DIMENSION_MM,
|
|
39
|
+
less_than_or_equal_to: MAX_PAPER_DIMENSION_MM }
|
|
40
|
+
validates :margin_top_mm, :margin_right_mm, :margin_bottom_mm, :margin_left_mm,
|
|
41
|
+
presence: true,
|
|
42
|
+
numericality: { greater_than_or_equal_to: MIN_MARGIN_MM,
|
|
43
|
+
less_than_or_equal_to: MAX_MARGIN_MM }
|
|
44
|
+
validate :margins_fit_within_paper
|
|
45
|
+
validate :layout_structure
|
|
46
|
+
|
|
47
|
+
def content_layout? = height_mode == 'content'
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def margins_fit_within_paper
|
|
52
|
+
return if paper_width_mm.blank? || paper_height_mm.blank?
|
|
53
|
+
|
|
54
|
+
if margin_left_mm.to_f + margin_right_mm.to_f >= paper_width_mm.to_f
|
|
55
|
+
errors.add(:base, 'Left and right margins together exceed the paper width')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
return if content_layout?
|
|
59
|
+
return if margin_top_mm.to_f + margin_bottom_mm.to_f < paper_height_mm.to_f
|
|
60
|
+
|
|
61
|
+
errors.add(:base, 'Top and bottom margins together exceed the paper height')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def layout_structure
|
|
65
|
+
return if layout.blank?
|
|
66
|
+
|
|
67
|
+
LayoutValidator.new(
|
|
68
|
+
layout: layout,
|
|
69
|
+
paper_width_mm: paper_width_mm.to_f,
|
|
70
|
+
paper_height_mm: paper_height_mm.to_f,
|
|
71
|
+
margin_top_mm: margin_top_mm.to_f,
|
|
72
|
+
margin_right_mm: margin_right_mm.to_f,
|
|
73
|
+
margin_bottom_mm: margin_bottom_mm.to_f,
|
|
74
|
+
margin_left_mm: margin_left_mm.to_f,
|
|
75
|
+
height_mode: height_mode
|
|
76
|
+
).call.each { |message| errors.add(:layout, message) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div class="nrb-receipt-element nrb-receipt-order-lines"
|
|
2
|
+
style="position: absolute; left: <%= presenter.x_mm %>mm; top: <%= presenter.y_mm %>mm; width: <%= presenter.width_mm %>mm; z-index: <%= presenter.z_index %>; font-size: <%= presenter.font_size_pt %>pt;">
|
|
3
|
+
<table class="nrb-receipt-order-lines-table" style="line-height: <%= presenter.text_line_height_mm %>mm;">
|
|
4
|
+
<colgroup>
|
|
5
|
+
<% presenter.columns.each do |column| %>
|
|
6
|
+
<col style="width: <%= presenter.column_width_pct(column) %>%;">
|
|
7
|
+
<% end %>
|
|
8
|
+
</colgroup>
|
|
9
|
+
|
|
10
|
+
<% if presenter.show_header? %>
|
|
11
|
+
<thead>
|
|
12
|
+
<tr class="nrb-receipt-order-lines-header">
|
|
13
|
+
<% presenter.columns.each do |column| %>
|
|
14
|
+
<th style="text-align: <%= column['align'] %>; padding-bottom: <%= presenter.row_spacing_mm %>mm;"><%= column['label'] %></th>
|
|
15
|
+
<% end %>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<% end %>
|
|
19
|
+
|
|
20
|
+
<tbody>
|
|
21
|
+
<% if presenter.empty? %>
|
|
22
|
+
<tr class="nrb-receipt-order-lines-empty">
|
|
23
|
+
<td colspan="<%= [presenter.columns.size, 1].max %>" class="text-secondary" style="padding-bottom: <%= presenter.row_spacing_mm %>mm;">No line items</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% else %>
|
|
26
|
+
<% presenter.rows.each do |row| %>
|
|
27
|
+
<tr>
|
|
28
|
+
<% presenter.columns.each do |column| %>
|
|
29
|
+
<td style="text-align: <%= column['align'] %>; padding-bottom: <%= presenter.row_spacing_mm %>mm;"><%= row[column['key']] %></td>
|
|
30
|
+
<% end %>
|
|
31
|
+
</tr>
|
|
32
|
+
<% end %>
|
|
33
|
+
<% end %>
|
|
34
|
+
</tbody>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<div class="alert alert-warning nrb-receipt-overflow-banner" role="alert">
|
|
2
|
+
<span class="material-symbols-outlined pe-1">warning</span>
|
|
3
|
+
This layout's content exceeds the paper height and may be cut off when printed.
|
|
4
|
+
Consider switching to "Grows with content" height mode or repositioning elements.
|
|
5
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<div class="nrb-receipt-paper-render" style="<%= paper_style(receipt_template, render_result) %>">
|
|
2
|
+
<% render_result.elements.each do |presenter| %>
|
|
3
|
+
<% case presenter.type %>
|
|
4
|
+
<% when 'variable' %>
|
|
5
|
+
<%= render 'nti_receipt_builder/render/variable_element', presenter: presenter %>
|
|
6
|
+
<% when 'text' %>
|
|
7
|
+
<%= render 'nti_receipt_builder/render/text_element', presenter: presenter %>
|
|
8
|
+
<% when 'order_lines' %>
|
|
9
|
+
<%= render 'nti_receipt_builder/render/order_lines_element', presenter: presenter %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="nrb-receipt-element" style="<%= presenter.css_style %>"><%= presenter.display_text %></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="nrb-receipt-element" style="<%= presenter.css_style %>"><%= presenter.display_text %></div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div id="receipt-canvas-panel" class="nrb-receipt-panel nrb-receipt-panel--canvas"
|
|
2
|
+
data-controller="nrb-canvas"
|
|
3
|
+
data-nrb-canvas-width-mm-value="<%= @receipt_template.paper_width_mm %>"
|
|
4
|
+
data-nrb-canvas-height-mm-value="<%= @receipt_template.paper_height_mm %>"
|
|
5
|
+
data-nrb-canvas-height-mode-value="<%= @receipt_template.height_mode %>"
|
|
6
|
+
data-nrb-canvas-margin-top-mm-value="<%= @receipt_template.margin_top_mm %>"
|
|
7
|
+
data-nrb-canvas-margin-right-mm-value="<%= @receipt_template.margin_right_mm %>"
|
|
8
|
+
data-nrb-canvas-margin-bottom-mm-value="<%= @receipt_template.margin_bottom_mm %>"
|
|
9
|
+
data-nrb-canvas-margin-left-mm-value="<%= @receipt_template.margin_left_mm %>"
|
|
10
|
+
data-nrb-canvas-grid-mm-value="2"
|
|
11
|
+
data-nrb-canvas-variable-labels-value="<%= NtiReceiptBuilder.variables_class.labels.to_json %>">
|
|
12
|
+
<div class="nrb-receipt-toolbar">
|
|
13
|
+
<h3 class="card-title mb-0">Canvas</h3>
|
|
14
|
+
<div class="nrb-receipt-zoom">
|
|
15
|
+
<span class="material-symbols-outlined">zoom_out</span>
|
|
16
|
+
<input type="range" min="0.5" max="2" step="0.1" value="1"
|
|
17
|
+
data-nrb-canvas-target="zoomRange"
|
|
18
|
+
data-action="input->nrb-canvas#zoomChanged">
|
|
19
|
+
<span class="material-symbols-outlined">zoom_in</span>
|
|
20
|
+
<span class="nrb-receipt-zoom-label" data-nrb-canvas-target="zoomLabel">100%</span>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="nrb-receipt-viewport" data-nrb-canvas-target="viewport"
|
|
25
|
+
data-action="pointerdown->nrb-canvas#paperClicked">
|
|
26
|
+
<div class="nrb-receipt-paper" data-nrb-canvas-target="paper">
|
|
27
|
+
<div class="nrb-receipt-paper-margins" data-nrb-canvas-target="marginGuides"></div>
|
|
28
|
+
<div class="nrb-receipt-paper-elements" data-nrb-canvas-target="elementsLayer"></div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<p class="text-secondary nrb-receipt-hint" data-nrb-builder-target="elementCount">0 elements</p>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="nrb-receipt-panel nrb-receipt-panel--elements">
|
|
2
|
+
<h3 class="card-title">Elements</h3>
|
|
3
|
+
<p class="text-secondary nrb-receipt-hint">
|
|
4
|
+
Drag an element onto the receipt, or click to drop it in the middle.
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<div class="nrb-receipt-palette-group">
|
|
8
|
+
<span class="nrb-receipt-palette-label">Variables</span>
|
|
9
|
+
<div class="nrb-receipt-palette">
|
|
10
|
+
<% NtiReceiptBuilder.variables_class.scalar_keys.each do |key| %>
|
|
11
|
+
<button type="button"
|
|
12
|
+
class="nrb-receipt-palette-btn"
|
|
13
|
+
data-nrb-builder-type-param="variable"
|
|
14
|
+
data-nrb-builder-key-param="<%= key %>"
|
|
15
|
+
data-action="pointerdown->nrb-builder#startPaletteDrag">
|
|
16
|
+
<span class="material-symbols-outlined">data_object</span>
|
|
17
|
+
<%= NtiReceiptBuilder.variables_class.labels[key] %>
|
|
18
|
+
</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="nrb-receipt-palette-group">
|
|
24
|
+
<span class="nrb-receipt-palette-label">Blocks</span>
|
|
25
|
+
<div class="nrb-receipt-palette">
|
|
26
|
+
<button type="button"
|
|
27
|
+
class="nrb-receipt-palette-btn"
|
|
28
|
+
data-nrb-builder-type-param="text"
|
|
29
|
+
data-action="pointerdown->nrb-builder#startPaletteDrag">
|
|
30
|
+
<span class="material-symbols-outlined">text_fields</span>
|
|
31
|
+
Custom Text
|
|
32
|
+
</button>
|
|
33
|
+
<% if (collection = NtiReceiptBuilder.variables_class.collection_definition) %>
|
|
34
|
+
<button type="button"
|
|
35
|
+
class="nrb-receipt-palette-btn"
|
|
36
|
+
data-nrb-builder-type-param="order_lines"
|
|
37
|
+
data-action="pointerdown->nrb-builder#startPaletteDrag">
|
|
38
|
+
<span class="material-symbols-outlined">receipt_long</span>
|
|
39
|
+
<%= collection.label %>
|
|
40
|
+
</button>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<p class="text-secondary nrb-receipt-hint">
|
|
46
|
+
Select an element on the canvas to edit it, or press
|
|
47
|
+
<code>Delete</code> to remove it.
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="alert alert-danger alert-dismissible" role="alert">
|
|
2
|
+
<div class="alert-icon">
|
|
3
|
+
<!-- Download SVG icon from http://tabler.io/icons/icon/alert-circle -->
|
|
4
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon alert-icon icon-2">
|
|
5
|
+
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0"></path>
|
|
6
|
+
<path d="M12 8v4"></path>
|
|
7
|
+
<path d="M12 16h.01"></path>
|
|
8
|
+
</svg>
|
|
9
|
+
</div>
|
|
10
|
+
<div>
|
|
11
|
+
<h4 class="alert-heading">Please review the following issue and try again:</h4>
|
|
12
|
+
<div class="alert-description">
|
|
13
|
+
<ul class="alert-list">
|
|
14
|
+
<% errors.each do |message| %>
|
|
15
|
+
<li><%= message %></li>
|
|
16
|
+
<% end %>
|
|
17
|
+
</ul>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Assets for the standalone print document. Both helpers are guarded so a host without
|
|
3
|
+
propshaft or importmap still renders a correct receipt — it just loses auto-print.
|
|
4
|
+
|
|
5
|
+
To control this precisely, shadow the partial from your own controller's view directory,
|
|
6
|
+
e.g. app/views/partner/receipt_templates/_print_assets.html.erb. Host prefixes are searched
|
|
7
|
+
before the gem's, so your copy wins with no configuration.
|
|
8
|
+
%>
|
|
9
|
+
<% sheets = Array(NtiReceiptBuilder.config.print_stylesheet).compact %>
|
|
10
|
+
<% if sheets.any? && respond_to?(:stylesheet_link_tag) %>
|
|
11
|
+
<%= stylesheet_link_tag(*sheets) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% if respond_to?(:javascript_importmap_tags) %>
|
|
14
|
+
<%= javascript_importmap_tags %>
|
|
15
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div id="receipt-properties-panel" class="nrb-receipt-panel nrb-receipt-panel--properties"
|
|
2
|
+
data-controller="nrb-properties"
|
|
3
|
+
data-nrb-properties-scalar-variables-value="<%= NtiReceiptBuilder.variables_class.scalar_keys.index_with { |key| NtiReceiptBuilder.variables_class.labels[key] }.to_json %>">
|
|
4
|
+
<h3 class="card-title">Properties</h3>
|
|
5
|
+
<div data-nrb-properties-target="content">
|
|
6
|
+
<p class="text-secondary nrb-receipt-hint">Select an element on the canvas to edit its properties.</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<div class="nrb-receipt-panel nrb-receipt-panel--settings">
|
|
2
|
+
<h3 class="card-title">Settings</h3>
|
|
3
|
+
|
|
4
|
+
<div class="form-group mb-3">
|
|
5
|
+
<%= f.label :name, 'Template Name', class: 'form-label' %>
|
|
6
|
+
<%= f.text_field :name, class: 'form-control' %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="form-group mb-3">
|
|
10
|
+
<%= f.label :paper_size_key, 'Paper Size', class: 'form-label' %>
|
|
11
|
+
<%= f.select :paper_size_key,
|
|
12
|
+
NtiReceiptBuilder::Template::PAPER_PRESETS.map { |key, preset| [preset[:label], key] },
|
|
13
|
+
{},
|
|
14
|
+
class: 'form-select',
|
|
15
|
+
data: {
|
|
16
|
+
nrb_builder_target: 'paperSizeSelect',
|
|
17
|
+
action: 'change->nrb-builder#paperPresetChanged'
|
|
18
|
+
} %>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="row g-3">
|
|
22
|
+
<div class="col">
|
|
23
|
+
<div class="form-group mb-3">
|
|
24
|
+
<%= f.label :paper_width_mm, 'Width (mm)', class: 'form-label' %>
|
|
25
|
+
<%= f.number_field :paper_width_mm, step: 0.1,
|
|
26
|
+
min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
|
|
27
|
+
max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
|
|
28
|
+
class: 'form-control',
|
|
29
|
+
data: {
|
|
30
|
+
nrb_builder_target: 'widthInput',
|
|
31
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
32
|
+
} %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="col">
|
|
36
|
+
<div class="form-group mb-3">
|
|
37
|
+
<%= f.label :paper_height_mm, 'Height (mm)', class: 'form-label' %>
|
|
38
|
+
<%= f.number_field :paper_height_mm, step: 0.1,
|
|
39
|
+
min: NtiReceiptBuilder::Template::MIN_PAPER_DIMENSION_MM,
|
|
40
|
+
max: NtiReceiptBuilder::Template::MAX_PAPER_DIMENSION_MM,
|
|
41
|
+
class: 'form-control',
|
|
42
|
+
data: {
|
|
43
|
+
nrb_builder_target: 'heightInput',
|
|
44
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
45
|
+
} %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="row g-3">
|
|
51
|
+
<div class="col">
|
|
52
|
+
<div class="form-group mb-3">
|
|
53
|
+
<%= f.label :orientation, 'Orientation', class: 'form-label' %>
|
|
54
|
+
<%= f.select :orientation,
|
|
55
|
+
NtiReceiptBuilder::Template::ORIENTATIONS.map { |value| [value.titleize, value] },
|
|
56
|
+
{},
|
|
57
|
+
class: 'form-select',
|
|
58
|
+
data: {
|
|
59
|
+
nrb_builder_target: 'orientationSelect',
|
|
60
|
+
action: 'change->nrb-builder#settingsChanged'
|
|
61
|
+
} %>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="col">
|
|
65
|
+
<div class="form-group mb-3">
|
|
66
|
+
<%= f.label :height_mode, 'Height Mode', class: 'form-label' %>
|
|
67
|
+
<%= f.select :height_mode,
|
|
68
|
+
[['Fixed Height', 'fixed'], ['Grows With Content', 'content']],
|
|
69
|
+
{},
|
|
70
|
+
class: 'form-select',
|
|
71
|
+
data: {
|
|
72
|
+
nrb_builder_target: 'heightModeSelect',
|
|
73
|
+
action: 'change->nrb-builder#settingsChanged'
|
|
74
|
+
} %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<label class="form-label">Margins (mm)</label>
|
|
80
|
+
<div class="row g-3">
|
|
81
|
+
<div class="col-6">
|
|
82
|
+
<div class="form-group mb-3">
|
|
83
|
+
<%= f.label :margin_top_mm, 'Top', class: 'form-label' %>
|
|
84
|
+
<%= f.number_field :margin_top_mm, step: 0.1,
|
|
85
|
+
min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
|
|
86
|
+
class: 'form-control',
|
|
87
|
+
data: {
|
|
88
|
+
nrb_builder_target: 'marginTopInput',
|
|
89
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
90
|
+
} %>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="col-6">
|
|
94
|
+
<div class="form-group mb-3">
|
|
95
|
+
<%= f.label :margin_right_mm, 'Right', class: 'form-label' %>
|
|
96
|
+
<%= f.number_field :margin_right_mm, step: 0.1,
|
|
97
|
+
min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
|
|
98
|
+
class: 'form-control',
|
|
99
|
+
data: {
|
|
100
|
+
nrb_builder_target: 'marginRightInput',
|
|
101
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
102
|
+
} %>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="col-6">
|
|
106
|
+
<div class="form-group mb-3">
|
|
107
|
+
<%= f.label :margin_bottom_mm, 'Bottom', class: 'form-label' %>
|
|
108
|
+
<%= f.number_field :margin_bottom_mm, step: 0.1,
|
|
109
|
+
min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
|
|
110
|
+
class: 'form-control',
|
|
111
|
+
data: {
|
|
112
|
+
nrb_builder_target: 'marginBottomInput',
|
|
113
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
114
|
+
} %>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="col-6">
|
|
118
|
+
<div class="form-group mb-3">
|
|
119
|
+
<%= f.label :margin_left_mm, 'Left', class: 'form-label' %>
|
|
120
|
+
<%= f.number_field :margin_left_mm, step: 0.1,
|
|
121
|
+
min: NtiReceiptBuilder::Template::MIN_MARGIN_MM, max: NtiReceiptBuilder::Template::MAX_MARGIN_MM,
|
|
122
|
+
class: 'form-control',
|
|
123
|
+
data: {
|
|
124
|
+
nrb_builder_target: 'marginLeftInput',
|
|
125
|
+
action: 'input->nrb-builder#settingsChanged'
|
|
126
|
+
} %>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<% content_for :title, "Edit #{@receipt_template.name}" %>
|
|
2
|
+
<% content_for :cardless, true %>
|
|
3
|
+
|
|
4
|
+
<%= form_with model: @receipt_template, url: url_for(action: :update, id: @receipt_template.id), method: :patch,
|
|
5
|
+
scope: :receipt_template,
|
|
6
|
+
data: {
|
|
7
|
+
controller: 'nrb-builder',
|
|
8
|
+
nrb_builder_layout_value: @receipt_template.layout.to_json,
|
|
9
|
+
nrb_builder_presets_value: NtiReceiptBuilder::Template::PAPER_PRESETS.to_json,
|
|
10
|
+
nrb_builder_collection_columns_value:
|
|
11
|
+
(NtiReceiptBuilder.variables_class.collection_definition&.default_columns_config || []).to_json,
|
|
12
|
+
nrb_builder_nrb_canvas_outlet: '#receipt-canvas-panel',
|
|
13
|
+
nrb_builder_nrb_properties_outlet: '#receipt-properties-panel',
|
|
14
|
+
action: 'formdata->nrb-builder#writeLayoutToFormData ' \
|
|
15
|
+
'receipt-element:moved->nrb-builder#elementMoved ' \
|
|
16
|
+
'receipt-element:resized->nrb-builder#elementResized ' \
|
|
17
|
+
'receipt-element:selected->nrb-builder#elementSelected ' \
|
|
18
|
+
'receipt-canvas:deselect->nrb-builder#deselect ' \
|
|
19
|
+
'receipt-properties:changed->nrb-builder#propertiesChanged ' \
|
|
20
|
+
'receipt-properties:delete-requested->nrb-builder#removeSelected'
|
|
21
|
+
} do |f| %>
|
|
22
|
+
|
|
23
|
+
<div class="nrb-receipt-topbar">
|
|
24
|
+
<div>
|
|
25
|
+
<span class="nrb-receipt-topbar-name"><%= @receipt_template.name %></span>
|
|
26
|
+
<span class="text-secondary"><%= @receipt_template.paper_width_mm %>mm × <%= @receipt_template.paper_height_mm %>mm</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="d-flex justify-content-end gap-2">
|
|
29
|
+
<%= f.button 'Live Preview', type: 'submit', class: 'btn btn-outline-secondary',
|
|
30
|
+
formaction: url_for(action: :render_preview, id: @receipt_template.id), formmethod: :post %>
|
|
31
|
+
<%= link_to url_for(action: :preview, id: @receipt_template.id),
|
|
32
|
+
class: 'btn btn-outline-secondary', target: '_blank', rel: 'noopener' do %>
|
|
33
|
+
<span class="material-symbols-outlined pe-1">visibility</span>Preview
|
|
34
|
+
<% end %>
|
|
35
|
+
<%= link_to 'Back to Templates', url_for(action: :index), class: 'btn btn-secondary' %>
|
|
36
|
+
<%= f.submit 'Save', class: 'btn btn-primary' %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div id="errors"></div>
|
|
41
|
+
|
|
42
|
+
<%= f.hidden_field :layout, value: @receipt_template.layout.to_json,
|
|
43
|
+
data: { nrb_builder_target: 'layoutField' } %>
|
|
44
|
+
<span id="lock_version_field"><%= render 'nti_receipt_builder/templates/lock_version_field', receipt_template: @receipt_template %></span>
|
|
45
|
+
|
|
46
|
+
<div class="nrb-receipt-builder">
|
|
47
|
+
<div class="nrb-receipt-builder-side">
|
|
48
|
+
<%= render 'nti_receipt_builder/templates/elements_panel' %>
|
|
49
|
+
<%= render 'nti_receipt_builder/templates/settings_panel', f: f %>
|
|
50
|
+
</div>
|
|
51
|
+
<%= render 'nti_receipt_builder/templates/canvas' %>
|
|
52
|
+
<%= render 'nti_receipt_builder/templates/properties_panel' %>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|