spree_cm_commissioner 1.16.0 → 1.17.0.pre.pre1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -8
  3. data/app/assets/images/cm-hangmeas-checkout_image.svg +63 -0
  4. data/app/assets/images/cm-hangmeas-failed.svg +56 -0
  5. data/app/assets/images/cm-hangmeas-loader.svg +50 -0
  6. data/app/assets/images/cm-hangmeas-success.svg +51 -0
  7. data/app/controllers/spree/admin/tenants_controller.rb +8 -0
  8. data/app/controllers/spree/api/v2/storefront/dynamic_field_options_controller.rb +28 -0
  9. data/app/controllers/spree/api/v2/storefront/guests_controller.rb +2 -1
  10. data/app/interactors/spree_cm_commissioner/create_event.rb +2 -1
  11. data/app/mailers/spree_cm_commissioner/team_invite_mailer.rb +13 -0
  12. data/app/models/concerns/spree_cm_commissioner/participation_type_bitwise.rb +47 -0
  13. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +4 -0
  14. data/app/models/spree_cm_commissioner/dynamic_field.rb +20 -0
  15. data/app/models/spree_cm_commissioner/dynamic_field_option.rb +18 -0
  16. data/app/models/spree_cm_commissioner/guest.rb +5 -3
  17. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +25 -0
  18. data/app/models/spree_cm_commissioner/invite_team.rb +44 -0
  19. data/app/models/spree_cm_commissioner/product_decorator.rb +2 -0
  20. data/app/models/spree_cm_commissioner/product_dynamic_field.rb +8 -0
  21. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  22. data/app/models/spree_cm_commissioner/user_decorator.rb +1 -0
  23. data/app/models/spree_cm_commissioner/user_taxon.rb +1 -0
  24. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  25. data/app/models/spree_cm_commissioner/vendor_place.rb +5 -7
  26. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +1 -0
  27. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +2 -0
  28. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_option_serializer.rb +11 -0
  29. data/app/serializers/spree_cm_commissioner/v2/storefront/dynamic_field_serializer.rb +13 -0
  30. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_dynamic_field_serializer.rb +13 -0
  31. data/app/serializers/spree_cm_commissioner/v2/storefront/guest_serializer.rb +3 -2
  32. data/app/serializers/spree_cm_commissioner/v2/storefront/product_dynamic_field_serializer.rb +13 -0
  33. data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +23 -15
  34. data/app/services/spree_cm_commissioner/user_roles_assigner.rb +0 -6
  35. data/app/views/spree/admin/tenants/_form.html.erb +253 -0
  36. data/app/views/spree_cm_commissioner/layouts/team_invite_mailer.html.erb +15 -0
  37. data/app/views/spree_cm_commissioner/team_invite_mailer/_mailer_stylesheets.html.erb +79 -0
  38. data/app/views/spree_cm_commissioner/team_invite_mailer/send_team_invite_email.html.erb +42 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/20250512072817_create_spree_cm_commissioner_invite_team.rb +15 -0
  41. data/db/migrate/20250605024523_add_participation_type_to_spree_taxon.rb +5 -0
  42. data/db/migrate/20250606020914_create_cm_dynamic_fields.rb +11 -0
  43. data/db/migrate/20250606020958_create_cm_guest_dynamic_fields.rb +11 -0
  44. data/db/migrate/20250606021044_create_cm_dynamic_field_options.rb +10 -0
  45. data/db/migrate/20250606021045_create_cm_product_dynamic_fields.rb +10 -0
  46. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +2 -2
  47. data/lib/spree_cm_commissioner/version.rb +1 -1
  48. metadata +29 -4
@@ -1,8 +1,6 @@
1
- require_dependency 'spree_cm_commissioner'
2
-
3
1
  module SpreeCmCommissioner
4
- class VendorPlace < ApplicationRecord
5
- enum place_type: { branch: 0, stop: 1, locations: 2 }
2
+ class VendorPlace < Base
3
+ enum place_type: { location: 0, branch: 1, stop: 2 }
6
4
  acts_as_list scope: :vendor
7
5
 
8
6
  belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
@@ -13,9 +11,9 @@ module SpreeCmCommissioner
13
11
 
14
12
  accepts_nested_attributes_for :place, allow_destroy: true
15
13
 
16
- scope :branches, -> { where(place_type: :branch) }
17
- scope :stops, -> { where(place_type: :stop) }
18
- scope :locations, -> { where(place_type: :locations) }
14
+ scope :branches, -> { where(place_type: :branch) }
15
+ scope :stops, -> { where(place_type: :stop) }
16
+ scope :locations, -> { where(place_type: :location) }
19
17
 
20
18
  def selected
21
19
  vendor.selected_place_references.include?(place&.reference)
@@ -7,6 +7,7 @@ module Spree
7
7
  base.has_many :product_kind_option_types, serializer: :option_type
8
8
  base.has_many :promoted_option_types, serializer: :option_type
9
9
  base.has_many :possible_promotions, serializer: ::SpreeCmCommissioner::V2::Storefront::PromotionSerializer
10
+ base.has_many :dynamic_fields, serializer: SpreeCmCommissioner::V2::Storefront::DynamicFieldSerializer
10
11
 
11
12
  base.has_one :default_state, serializer: :state
12
13
  base.has_one :venue, serializer: ::SpreeCmCommissioner::V2::Storefront::ProductPlaceSerializer
@@ -25,6 +25,8 @@ module Spree
25
25
  taxon.purchasable_on == 'web' || taxon.purchasable_on == 'both'
26
26
  end
27
27
 
28
+ base.attribute :participation_type_fields
29
+
28
30
  base.cache_options store: nil
29
31
 
30
32
  base.attribute :event_url, &:event_url
@@ -0,0 +1,11 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class DynamicFieldOptionSerializer < BaseSerializer
5
+ set_type :dynamic_field_option
6
+
7
+ attributes :id, :value, :dynamic_field_id, :created_at, :updated_at
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class DynamicFieldSerializer < BaseSerializer
5
+ set_type :dynamic_field
6
+
7
+ attributes :id, :label, :data_type, :vendor_id, :created_at, :updated_at
8
+
9
+ has_many :dynamic_field_options, serializer: SpreeCmCommissioner::V2::Storefront::DynamicFieldOptionSerializer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class GuestDynamicFieldSerializer < BaseSerializer
5
+ set_type :guest_dynamic_field
6
+
7
+ attributes :id, :value, :guest_id, :dynamic_field_id, :created_at, :updated_at
8
+
9
+ belongs_to :dynamic_field, serializer: SpreeCmCommissioner::V2::Storefront::DynamicFieldSerializer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -20,12 +20,13 @@ module SpreeCmCommissioner
20
20
 
21
21
  belongs_to :occupation, serializer: Spree::V2::Storefront::TaxonSerializer
22
22
  belongs_to :nationality, serializer: Spree::V2::Storefront::TaxonSerializer
23
+ belongs_to :line_item, serializer: Spree::V2::Storefront::LineItemSerializer
24
+
25
+ has_many :guest_dynamic_fields, serializer: SpreeCmCommissioner::V2::Storefront::GuestDynamicFieldSerializer
23
26
 
24
27
  has_one :id_card, serializer: Spree::V2::Storefront::IdCardSerializer
25
28
  has_one :check_in
26
29
 
27
- belongs_to :line_item, serializer: Spree::V2::Storefront::LineItemSerializer
28
-
29
30
  # allowed_checkout updates frequently
30
31
  cache_options store: nil
31
32
  end
@@ -0,0 +1,13 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class ProductDynamicFieldSerializer < BaseSerializer
5
+ set_type :product_dynamic_field
6
+
7
+ attributes :id, :product_id, :dynamic_field_id, :created_at, :updated_at
8
+
9
+ belongs_to :dynamic_field, serializer: SpreeCmCommissioner::V2::Storefront::DynamicFieldSerializer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,11 +1,12 @@
1
1
  module SpreeCmCommissioner
2
2
  module Organizer
3
3
  class ExportGuestCsvService
4
- attr_reader :event_id, :columns
4
+ attr_reader :event_id, :columns, :filters
5
5
 
6
- def initialize(event_id, columns)
6
+ def initialize(event_id, columns, filters = {})
7
7
  @event_id = event_id
8
8
  @columns = columns
9
+ @filters = filters
9
10
  end
10
11
 
11
12
  def call
@@ -18,7 +19,7 @@ module SpreeCmCommissioner
18
19
  private
19
20
 
20
21
  def headers
21
- columns.map(&:humanize).uniq
22
+ columns.map { |col| col.titleize(keep_id_suffix: true) }.uniq
22
23
  end
23
24
 
24
25
  def build_row(guest)
@@ -28,10 +29,8 @@ module SpreeCmCommissioner
28
29
  def fetch_value(guest, column)
29
30
  if guest_field?(column)
30
31
  fetch_guest_value(guest, column)
31
- elsif option_type_field?(column)
32
- fetch_option_value(guest, column)
33
32
  else
34
- ''
33
+ fetch_option_value(guest, column)
35
34
  end
36
35
  end
37
36
 
@@ -39,19 +38,21 @@ module SpreeCmCommissioner
39
38
  SpreeCmCommissioner::KycBitwise::ORDERED_BIT_FIELDS.include?(column.to_sym)
40
39
  end
41
40
 
42
- def option_type_field?(column)
43
- Spree::OptionType.exists?(name: column)
44
- end
45
-
46
41
  def fetch_guest_value(guest, column)
47
42
  column_mappings(guest)[column] || ''
48
43
  end
49
44
 
50
- def fetch_option_value(guest, option_type_name)
51
- return guest.formatted_bib_number if option_type_name == 'bib-prefix'
45
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
46
+ def fetch_option_value(guest, column_name)
47
+ return guest.formatted_bib_number if column_name == 'bib-prefix'
48
+ return guest.line_item&.number if column_name == 'item_id'
49
+ return guest.line_item&.order&.number if column_name == 'order_id'
50
+ return guest.created_at if column_name == 'created_at'
51
+ return guest.updated_at if column_name == 'updated_at'
52
52
 
53
- guest.line_item&.variant&.find_option_value_name_for(option_type_name: option_type_name)
53
+ guest.line_item&.variant&.find_option_value_name_for(option_type_name: column_name) || ''
54
54
  end
55
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
55
56
 
56
57
  def column_mappings(guest)
57
58
  {
@@ -77,12 +78,19 @@ module SpreeCmCommissioner
77
78
  end
78
79
 
79
80
  def guests_with_associations
80
- event.guests.includes(
81
+ scope = event.guests.includes(
81
82
  :occupation,
82
83
  :nationality,
83
84
  :id_card,
84
85
  line_item: { variant: { option_values: :option_type } }
85
- )
86
+ ).complete
87
+ scope = scope.where('cm_guests.created_at >= ?', formatted_date_time(filters[:from_date])) if filters[:from_date].present?
88
+ scope = scope.where('cm_guests.created_at <= ?', formatted_date_time(filters[:to_date])) if filters[:to_date].present?
89
+ scope
90
+ end
91
+
92
+ def formatted_date_time(date)
93
+ Time.zone.parse(date.to_s).strftime('%Y-%m-%d %H:%M:%S %z')
86
94
  end
87
95
 
88
96
  def event
@@ -19,17 +19,11 @@ module SpreeCmCommissioner
19
19
  end
20
20
 
21
21
  def create
22
- return { success: false, message: I18n.t('user_roles_assigner.user_not_found') } unless user
23
- return { success: false, message: I18n.t('user_roles_assigner.user_already_assigned') } if user.vendors.exists?(id: vendor_id)
24
- return { success: false, message: I18n.t('user_roles_assigner.roles_required') } if role_ids.blank?
25
-
26
22
  create_roles
27
23
  { success: true }
28
24
  end
29
25
 
30
26
  def update
31
- return { success: false, message: I18n.t('user_roles_assigner.roles_required') } if role_ids.blank?
32
-
33
27
  update_roles
34
28
  { success: true }
35
29
  end
@@ -92,3 +92,256 @@
92
92
  <% end %>
93
93
  </div>
94
94
  </div>
95
+ <div data-hook="admin_tenant_form_fields" class="row">
96
+
97
+ <!-- payment icon fields improved UI -->
98
+ <div class="col-3">
99
+ <div class="card mb-3">
100
+ <div class="card-body text-center">
101
+ <%= f.field_container :preferred_payment_checkout_image do %>
102
+ <%= f.label :payment_checkout_image, raw(Spree.t(:payment_checkout_image)), class: 'font-weight-bold' %>
103
+ <div class="mb-2 position-relative">
104
+ <img id="preview_preferred_payment_checkout_image"
105
+ src="<%= (f.object.preferred_payment_checkout_image.present? ? asset_path(f.object.preferred_payment_checkout_image) : asset_path('cm-hangmeas-checkout_image.svg')) %>"
106
+ alt="Icon Preview"
107
+ class="img-thumbnail bg-light border payment-icon-preview"
108
+ data-toggle="tooltip" title="Click to select icon"
109
+ onclick="$('#vector-icons-modal').modal('show'); setOptionValueId('preferred_payment_checkout_image'); setCurrentSelectedIcon(document.getElementById('preferred_payment_checkout_image').value);">
110
+ <button type="button" class="btn btn-sm btn-outline-danger position-absolute" style="top:5px; right:5px;" title="Remove icon" onclick="document.getElementById('preferred_payment_checkout_image').value=''; document.getElementById('preview_preferred_payment_checkout_image').src='<%= asset_path('cm-hangmeas-checkout_image.svg') %>'; document.getElementById('preview_preferred_payment_checkout_image').style.display='inline'; return false;">
111
+ <i class="fa fa-times"></i>
112
+ </button>
113
+ </div>
114
+ <div class="input-group">
115
+ <%= f.text_field :preferred_payment_checkout_image, class: 'form-control', placeholder: 'eg. https://example.com/icon.png', id: 'preferred_payment_checkout_image', 'aria-label': 'Payment Checkout Image' %>
116
+ <div class="input-group-append">
117
+ <button type="button" class="btn btn-outline-secondary option-value-toggler" data-toggle="modal" data-target="#vector-icons-modal" data-id="preferred_payment_checkout_image" data-vector-icon="<%= f.object.preferred_payment_checkout_image %>" title="Select icon">
118
+ <i class="fa fa-image"></i>
119
+ </button>
120
+ </div>
121
+ </div>
122
+ <%= f.error_message_on :preferred_payment_checkout_image %>
123
+ <% end %>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="col-3">
129
+ <div class="card mb-3">
130
+ <div class="card-body text-center">
131
+ <%= f.field_container :preferred_payment_failed_image do %>
132
+ <%= f.label :payment_failed_image, raw(Spree.t(:payment_failed_image)), class: 'font-weight-bold' %>
133
+ <div class="mb-2 position-relative">
134
+ <img id="preview_preferred_payment_failed_image"
135
+ src="<%= (f.object.preferred_payment_failed_image.present? ? asset_path(f.object.preferred_payment_failed_image) : asset_path('cm-hangmeas-failed.svg')) %>"
136
+ alt="Icon Preview"
137
+ class="img-thumbnail bg-light border payment-icon-preview"
138
+ data-toggle="tooltip" title="Click to select icon"
139
+ onclick="$('#vector-icons-modal').modal('show'); setOptionValueId('preferred_payment_failed_image'); setCurrentSelectedIcon(document.getElementById('preferred_payment_failed_image').value);">
140
+ <button type="button" class="btn btn-sm btn-outline-danger position-absolute" style="top:5px; right:5px;" title="Remove icon" onclick="document.getElementById('preferred_payment_failed_image').value=''; document.getElementById('preview_preferred_payment_failed_image').src='<%= asset_path('cm-hangmeas-failed.svg') %>'; document.getElementById('preview_preferred_payment_failed_image').style.display='inline'; return false;">
141
+ <i class="fa fa-times"></i>
142
+ </button>
143
+ </div>
144
+ <div class="input-group">
145
+ <%= f.text_field :preferred_payment_failed_image, class: 'form-control', placeholder: 'eg. https://example.com/icon.png', id: 'preferred_payment_failed_image', 'aria-label': 'Payment Failed Image' %>
146
+ <div class="input-group-append">
147
+ <button type="button" class="btn btn-outline-secondary option-value-toggler" data-toggle="modal" data-target="#vector-icons-modal" data-id="preferred_payment_failed_image" data-vector-icon="<%= f.object.preferred_payment_failed_image %>" title="Select icon">
148
+ <i class="fa fa-image"></i>
149
+ </button>
150
+ </div>
151
+ </div>
152
+ <%= f.error_message_on :preferred_payment_failed_image %>
153
+ <% end %>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="col-3">
159
+ <div class="card mb-3">
160
+ <div class="card-body text-center">
161
+ <%= f.field_container :preferred_payment_success_image do %>
162
+ <%= f.label :payment_success_image, raw(Spree.t(:payment_success_image)), class: 'font-weight-bold' %>
163
+ <div class="mb-2 position-relative">
164
+ <img id="preview_preferred_payment_success_image"
165
+ src="<%= (f.object.preferred_payment_success_image.present? ? asset_path(f.object.preferred_payment_success_image) : asset_path('cm-hangmeas-success.svg')) %>"
166
+ alt="Icon Preview"
167
+ class="img-thumbnail bg-light border payment-icon-preview"
168
+ data-toggle="tooltip" title="Click to select icon"
169
+ onclick="$('#vector-icons-modal').modal('show'); setOptionValueId('preferred_payment_success_image'); setCurrentSelectedIcon(document.getElementById('preferred_payment_success_image').value);">
170
+ <button type="button" class="btn btn-sm btn-outline-danger position-absolute" style="top:5px; right:5px;" title="Remove icon" onclick="document.getElementById('preferred_payment_success_image').value=''; document.getElementById('preview_preferred_payment_success_image').src='<%= asset_path('cm-hangmeas-success.svg') %>'; document.getElementById('preview_preferred_payment_success_image').style.display='inline'; return false;">
171
+ <i class="fa fa-times"></i>
172
+ </button>
173
+ </div>
174
+ <div class="input-group">
175
+ <%= f.text_field :preferred_payment_success_image, class: 'form-control', placeholder: 'eg. https://example.com/icon.png', id: 'preferred_payment_success_image', 'aria-label': 'Payment Success Image' %>
176
+ <div class="input-group-append">
177
+ <button type="button" class="btn btn-outline-secondary option-value-toggler" data-toggle="modal" data-target="#vector-icons-modal" data-id="preferred_payment_success_image" data-vector-icon="<%= f.object.preferred_payment_success_image %>" title="Select icon">
178
+ <i class="fa fa-image"></i>
179
+ </button>
180
+ </div>
181
+ </div>
182
+ <%= f.error_message_on :preferred_payment_success_image %>
183
+ <% end %>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="col-3">
189
+ <div class="card mb-3">
190
+ <div class="card-body text-center">
191
+ <%= f.field_container :preferred_payment_loader do %>
192
+ <%= f.label :payment_loader, raw(Spree.t(:payment_loader)), class: 'font-weight-bold' %>
193
+ <div class="mb-2 position-relative">
194
+ <img id="preview_preferred_payment_loader"
195
+ src="<%= (f.object.preferred_payment_loader.present? ? asset_path(f.object.preferred_payment_loader) : asset_path('cm-hangmeas-loader.svg')) %>"
196
+ alt="Icon Preview"
197
+ class="img-thumbnail bg-light border payment-icon-preview"
198
+ data-toggle="tooltip" title="Click to select icon"
199
+ onclick="$('#vector-icons-modal').modal('show'); setOptionValueId('preferred_payment_loader'); setCurrentSelectedIcon(document.getElementById('preferred_payment_loader').value);">
200
+ <button type="button" class="btn btn-sm btn-outline-danger position-absolute" style="top:5px; right:5px;" title="Remove icon" onclick="document.getElementById('preferred_payment_loader').value=''; document.getElementById('preview_preferred_payment_loader').src='<%= asset_path('cm-hangmeas-loader.svg') %>'; document.getElementById('preview_preferred_payment_loader').style.display='inline'; return false;">
201
+ <i class="fa fa-times"></i>
202
+ </button>
203
+ </div>
204
+ <div class="input-group">
205
+ <%= f.text_field :preferred_payment_loader, class: 'form-control', placeholder: 'eg. https://example.com/icon.png', id: 'preferred_payment_loader', 'aria-label': 'Payment Loader Image' %>
206
+ <div class="input-group-append">
207
+ <button type="button" class="btn btn-outline-secondary option-value-toggler" data-toggle="modal" data-target="#vector-icons-modal" data-id="preferred_payment_loader" data-vector-icon="<%= f.object.preferred_payment_loader %>" title="Select icon">
208
+ <i class="fa fa-image"></i>
209
+ </button>
210
+ </div>
211
+ </div>
212
+ <%= f.error_message_on :preferred_payment_loader %>
213
+ <% end %>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ </div>
219
+
220
+
221
+ <!-- Vector Icon Selector Modal -->
222
+ <div class="modal fade" id="vector-icons-modal" tabindex="-1" role="dialog" aria-hidden="true">
223
+ <div class="modal-dialog modal-dialog-centered" role="document">
224
+ <div class="modal-content">
225
+ <div class="modal-header">
226
+ <h5 class="modal-title">Icons</h5>
227
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
228
+ <span aria-hidden="true">&times;</span>
229
+ </button>
230
+ </div>
231
+ <div class="modal-body" data-hook="admin_product_form_vector_icons">
232
+ <div class="form-group">
233
+ <label for="vector_icon_select">Select Icon</label>
234
+ <select id="vector_icon_select" class="form-control select2">
235
+ <option value="">None</option>
236
+ <% @vector_icons&.each do |icon| %>
237
+ <option value="<%= icon.path %>"><%= icon.path %></option>
238
+ <% end %>
239
+ </select>
240
+ </div>
241
+ </div>
242
+ <div class="modal-footer">
243
+ <input type="hidden" id="modal-option-value-id" value="" />
244
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
245
+ <button type="button" class="btn btn-primary" id="select-vector-icon-btn">Select</button>
246
+ </div>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <script>
252
+ function setCurrentSelectedIcon(selectVectorIcon) {
253
+ var optionsContainer = document.getElementById("vector_icon_select");
254
+ if (!optionsContainer) return;
255
+ var options = Array.from(optionsContainer.options).map((e) => e.value || e.innerHTML);
256
+ var index = Math.max(options.indexOf(selectVectorIcon), 0);
257
+ optionsContainer.selectedIndex = index;
258
+ }
259
+
260
+ function setOptionValueId(id) {
261
+ document.getElementById("modal-option-value-id").value = id;
262
+ }
263
+
264
+ document.body.addEventListener('click', function(e){
265
+ var target = e.target;
266
+ if (target.classList.contains('option-value-toggler')) {
267
+ setOptionValueId(target.dataset.id);
268
+ setCurrentSelectedIcon(target.dataset.vectorIcon);
269
+ }
270
+ });
271
+
272
+ function getAssetUrl(path) {
273
+ if (!path) return '';
274
+ if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/assets/')) return path;
275
+ return '/assets/' + path;
276
+ }
277
+
278
+ document.getElementById('select-vector-icon-btn').addEventListener('click', function() {
279
+ var fieldId = document.getElementById("modal-option-value-id").value;
280
+ var selectedIcon = document.getElementById("vector_icon_select").value;
281
+
282
+ if (fieldId) {
283
+ document.getElementById(fieldId).value = selectedIcon;
284
+
285
+ var previewImg = document.getElementById('preview_' + fieldId);
286
+ if (previewImg) {
287
+ if (selectedIcon) {
288
+ previewImg.src = getAssetUrl(selectedIcon);
289
+ previewImg.style.display = 'inline';
290
+ } else {
291
+ previewImg.src = '';
292
+ previewImg.style.display = 'none';
293
+ }
294
+ }
295
+ }
296
+
297
+ // close select2 if open
298
+ $('#vector_icon_select').select2('close');
299
+
300
+ // hide modal
301
+ $('#vector-icons-modal').modal('hide');
302
+ });
303
+
304
+ $(document).ready(function() {
305
+ $('#vector_icon_select').select2({
306
+ dropdownParent: $('#vector-icons-modal')
307
+ });
308
+
309
+ // Auto-close modal & save immediately when selecting an option
310
+ $('#vector_icon_select').on('select2:select', function (e) {
311
+ $('#select-vector-icon-btn').trigger('click');
312
+ });
313
+ });
314
+
315
+ // Live preview when typing URLs manually
316
+ [
317
+ 'preferred_payment_checkout_image',
318
+ 'preferred_payment_failed_image',
319
+ 'preferred_payment_success_image',
320
+ 'preferred_payment_loader'
321
+ ].forEach(function(fieldId) {
322
+ var input = document.getElementById(fieldId);
323
+ if (input) {
324
+ input.addEventListener('input', function() {
325
+ var previewImg = document.getElementById('preview_' + fieldId);
326
+ if (previewImg) {
327
+ if (input.value) {
328
+ previewImg.src = getAssetUrl(input.value);
329
+ previewImg.style.display = 'inline';
330
+ } else {
331
+ previewImg.src = '';
332
+ previewImg.style.display = 'none';
333
+ }
334
+ }
335
+ });
336
+ }
337
+ });
338
+ </script>
339
+
340
+ <style>
341
+ .payment-icon-preview {
342
+ max-height: 60px;
343
+ max-width: 120px;
344
+ cursor: pointer;
345
+ }
346
+ </style>
347
+
@@ -0,0 +1,15 @@
1
+
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
+ <html>
4
+ <head>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <meta name="x-apple-disable-message-reformatting"/>
7
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
8
+ <link href="https://fonts.googleapis.com/css2?family=Poppins&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
10
+ <%= render partial: 'spree_cm_commissioner/team_invite_mailer/mailer_stylesheets' %>
11
+ </head>
12
+ <body>
13
+ <%= yield %>
14
+ </body>
15
+ </html>
@@ -0,0 +1,79 @@
1
+ <style>
2
+ body, table, td {
3
+ font-family: 'Poppins', Arial, sans-serif;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ .content-wrapper {
9
+ max-width: 500px;
10
+ margin: 0 auto;
11
+ border: 1px solid grey;
12
+ border-top: none;
13
+ }
14
+
15
+ .main-content{
16
+ padding-top: 10px;
17
+ padding-bottom: 25px;
18
+ }
19
+
20
+ .heading-1 {
21
+ font-size: 24px;
22
+ text-align: center;
23
+ line-height: 1.4;
24
+ margin-top: 10px;
25
+ margin-bottom: 15px;
26
+ padding-left: 5px;
27
+ padding-right: 5px;
28
+
29
+ }
30
+
31
+ .bar-large {
32
+ height: 10px;
33
+ background-color: #5D54D9;
34
+ margin-bottom: 20px;
35
+
36
+ }
37
+
38
+ .bar-thin {
39
+ height: 1px;
40
+ background-color: grey;
41
+ width: 100%;
42
+ margin: 20px auto;
43
+ }
44
+
45
+ .heading-2 {
46
+ font-size: 16px;
47
+ text-align: center;
48
+ line-height: 1.5;
49
+ }
50
+
51
+ .title-bold {
52
+ font-weight: bold;
53
+ color: #000;
54
+ }
55
+
56
+ .action-button {
57
+ display: inline-block;
58
+ background-color: #5D54D9;
59
+ border-radius: 24px;
60
+ font-size: 14px;
61
+ width: 222px;
62
+ height: 50px;
63
+ text-align: center;
64
+ line-height: 50px;
65
+ text-decoration: none;
66
+ cursor: pointer;
67
+ margin: 15px 0;
68
+ color: white !important;
69
+ text-decoration: none;
70
+ }
71
+
72
+ .heading-3 {
73
+ font-size: 12px;
74
+ text-align: center;
75
+ margin-bottom: 15px;
76
+ color: #888;
77
+ }
78
+ </style>
79
+
@@ -0,0 +1,42 @@
1
+ <table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center" width="100%">
2
+ <tr>
3
+ <td align="center">
4
+ <div class="content-wrapper">
5
+ <div class="bar-large"></div>
6
+ <div class="main-content">
7
+ <div class="content">
8
+ <div>
9
+ <%= image_tag(main_app.rails_blob_url(Spree::StoreLogo.first.attachment), style: "width: 120px; height: 120px;") %>
10
+ </div>
11
+ <div class="heading-1">
12
+ You're invited to join
13
+ <span class="title-bold">
14
+ BookMe+
15
+ </span>
16
+ Business.
17
+ </div>
18
+ <div class="bar-thin"></div>
19
+ <div class="heading-2">
20
+ <span class="title-bold"><%= @invite_team.inviter.full_name.presence || @invite_team.inviter.email %></span> has invited you to manage bookings and grow together on the platform. <br>
21
+ </div>
22
+ <div style="text-align: center;">
23
+ <a href="<%= @invite_team.invitation_link %>" class="action-button">
24
+ Accept Invitation
25
+ </a>
26
+ </div>
27
+
28
+ <div class="heading-3">
29
+ Use this email <span class="title-bold"><%= @invite_team.email %></span> to log in or <br>create an account on BookMe+ Platform
30
+ </div>
31
+
32
+ <div class="heading-3">
33
+ This invitation will expire<br>
34
+ in <%= ((@invite_team.expires_at - Time.current) / 1.hour).round %> hours.
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </td>
40
+ </tr>
41
+ </table>
42
+
data/config/routes.rb CHANGED
@@ -619,6 +619,7 @@ Spree::Core::Engine.add_routes do
619
619
  resources :guests, only: %i[create update show] do
620
620
  resources :id_cards
621
621
  end
622
+ resources :dynamic_field_options
622
623
  resources :user_guests do
623
624
  resources :id_cards
624
625
  end
@@ -0,0 +1,15 @@
1
+ class CreateSpreeCmCommissionerInviteTeam < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :cm_invite_teams, if_not_exists: true do |t|
4
+ t.string :email, null: false
5
+ t.string :token, null: false, unique: true, index: true
6
+ t.references :inviter, null: false, foreign_key: { to_table: :spree_users }, index: true
7
+ t.references :vendor, null:false, foreign_key: { to_table: :spree_vendors }, index: true
8
+ t.integer :status, default: 0, null: false
9
+ t.text :preferences
10
+ t.datetime :expires_at
11
+ t.datetime :confirmed_at
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class AddParticipationTypeToSpreeTaxon < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :spree_taxons , :participation_type, :integer, default: 0, null: false, if_not_exists: true
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class CreateCmDynamicFields < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :cm_dynamic_fields, if_not_exists: true do |t|
4
+ t.string :label, null: false
5
+ t.integer :data_type, null: false
6
+ t.references :vendor, null: false, foreign_key: { to_table: :spree_vendors }
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end