spree_cm_commissioner 2.3.0.pre.pre16 → 2.3.0.pre.pre17

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +33 -0
  4. data/app/controllers/blazer/base_controller_decorator.rb +20 -1
  5. data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +40 -0
  6. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +47 -9
  7. data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +1 -1
  8. data/app/factory/spree_cm_commissioner/vendor_telegram_message_factory.rb +65 -0
  9. data/app/helpers/spree/base_helper_decorator.rb +2 -19
  10. data/app/interactors/spree_cm_commissioner/vendor_creation_telegram_alert_sender.rb +28 -0
  11. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +1 -1
  12. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +1 -1
  13. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +1 -1
  14. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +1 -1
  15. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +1 -1
  16. data/app/jobs/spree_cm_commissioner/vendor_creation_telegram_alert_sender_job.rb +10 -0
  17. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +51 -0
  18. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +7 -0
  19. data/app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb +25 -0
  20. data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +3 -14
  21. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +18 -4
  22. data/app/models/spree_cm_commissioner/block.rb +19 -0
  23. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  24. data/app/models/spree_cm_commissioner/variant_decorator.rb +0 -5
  25. data/app/models/spree_cm_commissioner/variant_options.rb +8 -0
  26. data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
  27. data/app/overrides/spree/admin/variants/_form/add_permanent_stock.html.erb.deface +2 -1
  28. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_cart_serializer.rb +12 -0
  29. data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +31 -0
  30. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +11 -0
  31. data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +67 -0
  32. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +79 -0
  33. data/app/services/spree_cm_commissioner/{transit/base_route_order_metrics_updater.rb → routes/base_update_order_metrics.rb} +2 -2
  34. data/app/services/spree_cm_commissioner/{transit/route_previous_trip_count_decrementer_service.rb → routes/decrement_previous_trip_count.rb} +3 -3
  35. data/app/services/spree_cm_commissioner/{transit/route_trip_count_decrementer_service.rb → routes/decrement_trip_count.rb} +2 -2
  36. data/app/services/spree_cm_commissioner/{transit/route_fulfilled_order_count_incrementer_service.rb → routes/increment_fulfilled_order_count.rb} +3 -3
  37. data/app/services/spree_cm_commissioner/{transit/route_order_count_incrementer_service.rb → routes/increment_order_count.rb} +3 -3
  38. data/app/services/spree_cm_commissioner/{transit/route_trip_count_incrementer_service.rb → routes/increment_trip_count.rb} +2 -2
  39. data/app/services/spree_cm_commissioner/trips/search.rb +65 -0
  40. data/app/views/blazer/queries/embed/_content.html.erb +51 -2
  41. data/app/views/spree/admin/option_types/_color_field.html.erb +21 -0
  42. data/app/views/spree/admin/option_types/_option_value_fields.html.erb +2 -0
  43. data/app/views/spree/admin/variants/_color_field.html.erb +28 -0
  44. data/app/views/spree/admin/variants/_date_field.html.erb +11 -1
  45. data/app/views/spree/admin/variants/_default_field.html.erb +7 -3
  46. data/app/views/spree/admin/variants/_option_values.html.erb +5 -1
  47. data/app/views/spree/admin/variants/_time_field.html.erb +7 -1
  48. data/app/views/spree/order_mailer/_adjustment.html.erb +7 -0
  49. data/app/views/spree_cm_commissioner/event_transactional_mailer/_event_banner.html.erb +9 -3
  50. data/config/locales/km.yml +64 -0
  51. data/config/routes.rb +4 -1
  52. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +18 -0
  53. data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +30 -0
  54. data/lib/spree_cm_commissioner/trip_result.rb +15 -0
  55. data/lib/spree_cm_commissioner/version.rb +1 -1
  56. metadata +20 -8
@@ -68,11 +68,6 @@ module SpreeCmCommissioner
68
68
  )
69
69
  end
70
70
 
71
- # override
72
- def options_text
73
- @options_text ||= Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
74
- end
75
-
76
71
  def selected_option_value_ids
77
72
  option_value_variants.pluck(:option_value_id)
78
73
  end
@@ -139,6 +139,14 @@ module SpreeCmCommissioner
139
139
  @seat_number_layouts ||= option_value_name_for(option_type_name: 'seat-number-layouts')&.split(',')
140
140
  end
141
141
 
142
+ def color
143
+ @color ||= option_value_name_for(option_type_name: 'color')
144
+ end
145
+
146
+ def ticket_type
147
+ @ticket_type ||= option_value_name_for(option_type_name: 'ticket-type')
148
+ end
149
+
142
150
  def seat_type
143
151
  @seat_type ||= option_value_name_for(option_type_name: 'seat-type')
144
152
  end
@@ -16,6 +16,8 @@ module SpreeCmCommissioner
16
16
 
17
17
  base.before_save :generate_code, if: :code.nil?
18
18
 
19
+ base.after_create :send_telegram_vendor_creation_alert
20
+
19
21
  base.has_many :photos, -> { order(:position) }, as: :viewable, dependent: :destroy, class_name: 'SpreeCmCommissioner::VendorPhoto'
20
22
  base.has_many :option_values, through: :products
21
23
  base.has_many :vendor_option_types, class_name: 'SpreeCmCommissioner::VendorOptionType'
@@ -201,6 +203,10 @@ module SpreeCmCommissioner
201
203
  vendor_kind_option_values.where(option_type_id: rules_option_type.id)
202
204
  end
203
205
 
206
+ def send_telegram_vendor_creation_alert
207
+ VendorCreationTelegramAlertSenderJob.perform_later(id)
208
+ end
209
+
204
210
  delegate :present?, to: :tenant, prefix: true
205
211
  end
206
212
  end
@@ -1,6 +1,7 @@
1
1
  <!-- insert_bottom "[data-hook='admin_variant_form_additional_fields']" -->
2
2
 
3
- <div class="form-group">
3
+ <%# deprecated no longer used, should be removed entirely along with table column, just hide for now. %>
4
+ <div class="form-group d-none">
4
5
  <%= f.field_container :permanent_stock do %>
5
6
  <%= f.label :permanent_stock, Spree.t(:permanent_stock) %>
6
7
  <%= f.number_field :permanent_stock, class: 'form-control' %>
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class IntercityTaxiCartSerializer < Spree::V2::Storefront::CartSerializer
5
+ has_many :line_items,
6
+ serializer: SpreeCmCommissioner::V2::Storefront::IntercityTaxiLineItemSerializer
7
+
8
+ cache_options store: nil
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ class IntercityTaxiLineItemSerializer < BaseSerializer
5
+ attributes :pickup_place_name,
6
+ :drop_off_place_name,
7
+ :pickup_lat,
8
+ :pickup_lng,
9
+ :drop_off_lat,
10
+ :drop_off_lng,
11
+ :passenger_count,
12
+ :distance_km,
13
+ :ordered_points,
14
+ :base_km,
15
+ :detour_pickup_km,
16
+ :detour_dropoff_km,
17
+ :extra_pickup_km,
18
+ :extra_dropoff_km,
19
+ :extra_pickup_charge_usd,
20
+ :extra_dropoff_charge_usd,
21
+ :estimated_time_minutes,
22
+ :from_date,
23
+ :to_date,
24
+ :direction,
25
+ :trip_id
26
+
27
+ cache_options store: nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,6 +4,17 @@ module SpreeCmCommissioner
4
4
  class TripResultSerializer < BaseSerializer
5
5
  set_type :trip_result
6
6
 
7
+ # Enable serializer-level caching
8
+ # Longer TTL than controller cache (5min) because:
9
+ # - Individual trips change less frequently
10
+ # - Same trip can be reused across multiple searches
11
+ # - Cache invalidation handled by cache_key_with_version
12
+ cache_options(
13
+ store: Rails.cache,
14
+ namespace: 'trip-result-serializer',
15
+ expires_in: 30.minutes
16
+ )
17
+
7
18
  attributes :origin_place, :destination_place, :boarding, :drop_off, :price, :compare_at_amount, :currency, :quantity_available, :max_capacity,
8
19
  :allow_seat_selection, :departure_time, :route_type, :distance
9
20
  attribute :duration_in_hms, &:duration_in_hms
@@ -0,0 +1,67 @@
1
+ module SpreeCmCommissioner
2
+ module IntercityTaxiOrder
3
+ class Create
4
+ attr_reader :trip_id, :from_date, :to_date, :user_id, :quantity
5
+
6
+ def self.call(trip_id:, from_date:, to_date:, user_id: nil, quantity: 1)
7
+ new(
8
+ trip_id: trip_id,
9
+ from_date: from_date,
10
+ to_date: to_date,
11
+ user_id: user_id,
12
+ quantity: quantity
13
+ ).call
14
+ end
15
+
16
+ def initialize(trip_id:, from_date:, to_date:, user_id:, quantity:)
17
+ @trip_id = trip_id
18
+ @from_date = from_date
19
+ @to_date = to_date
20
+ @user_id = user_id
21
+ @quantity = quantity.to_i <= 0 ? 1 : quantity.to_i
22
+ end
23
+
24
+ def call
25
+ validate!
26
+
27
+ ActiveRecord::Base.transaction do
28
+ trip = SpreeCmCommissioner::Trip.find(trip_id)
29
+ variant = trip.product.variants.first
30
+ raise ArgumentError, 'No variant found for trip' if variant.blank?
31
+
32
+ order = Spree::Order.new(state: 'cart', use_billing: true, user_id: user_id)
33
+
34
+ line_item = order.line_items.new(
35
+ variant_id: variant.id,
36
+ quantity: quantity,
37
+ from_date: from_date,
38
+ to_date: to_date,
39
+ private_metadata: {
40
+ trip_id: trip.id
41
+ }
42
+ )
43
+
44
+ build_guests_for!(line_item, quantity)
45
+
46
+ order.save!
47
+ order.update_with_updater!
48
+
49
+ order
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def validate!
56
+ raise ArgumentError, 'trip_id is required' if trip_id.blank?
57
+ raise ArgumentError, 'from_date is required' if from_date.blank?
58
+ raise ArgumentError, 'to_date is required' if to_date.blank?
59
+ raise ArgumentError, 'to_date must be after from_date' if from_date.present? && to_date.present? && to_date < from_date
60
+ end
61
+
62
+ def build_guests_for!(line_item, qty)
63
+ Array.new(qty) { line_item.guests.new }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,79 @@
1
+ module SpreeCmCommissioner
2
+ module IntercityTaxiOrder
3
+ class Update
4
+ attr_reader :order_number, :params
5
+
6
+ def self.call(order_number:, params:)
7
+ new(order_number: order_number, params: params).call
8
+ end
9
+
10
+ def initialize(order_number:, params:)
11
+ @order_number = order_number
12
+ @params = ensure_parameters(params)
13
+ end
14
+
15
+ def call
16
+ order = Spree::Order.find_by!(number: order_number)
17
+ order.update!(order_params)
18
+
19
+ create_billing_address_if_missing(order)
20
+
21
+ order
22
+ end
23
+
24
+ private
25
+
26
+ def ensure_parameters(params)
27
+ return params if params.is_a?(::ActionController::Parameters)
28
+
29
+ ::ActionController::Parameters.new(params)
30
+ end
31
+
32
+ def create_billing_address_if_missing(order)
33
+ return if order.bill_address.present?
34
+
35
+ SpreeCmCommissioner::BillingAddressCreator.call(order: order)
36
+ end
37
+
38
+ def order_params
39
+ params.require(:order).permit(
40
+ :intel_phone_number,
41
+ :email,
42
+ line_items_attributes: [
43
+ :id,
44
+ :pickup_place_name,
45
+ :drop_off_place_name,
46
+ :from_date,
47
+ :pickup_lat,
48
+ :pickup_lng,
49
+ :pickup_oob_confirmed,
50
+ :drop_off_lat,
51
+ :drop_off_lng,
52
+ :drop_off_oob_confirmed,
53
+ :distance_km,
54
+ :ordered_points,
55
+ :base_km,
56
+ :detour_pickup_km,
57
+ :detour_dropoff_km,
58
+ :extra_pickup_km,
59
+ :extra_dropoff_km,
60
+ :extra_pickup_charge_usd,
61
+ :extra_dropoff_charge_usd,
62
+ :estimated_time_minutes,
63
+ :remark,
64
+ :passenger_count,
65
+ { guests_attributes: %i[
66
+ id
67
+ first_name
68
+ last_name
69
+ gender
70
+ nationality_id
71
+ age
72
+ ]
73
+ }
74
+ ]
75
+ )
76
+ end
77
+ end
78
+ end
79
+ end
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class BaseRouteOrderMetricsUpdater
2
+ module Routes
3
+ class BaseUpdateOrderMetrics
4
4
  attr_reader :order, :attribute
5
5
 
6
6
  SUPPORTED_ATTRIBUTES = %i[order_count fulfilled_order_count].freeze
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class RoutePreviousTripCountDecrementerService
2
+ module Routes
3
+ class DecrementPreviousTripCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  def call(previous_route_id:)
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
12
12
  # Build a trip-like object that responds to :route and :vendor so the delegated
13
13
  # service can locate vendor routes correctly.
14
14
  trip_like = Struct.new(:route, :vendor).new(previous_route, previous_route.vendors.first)
15
- result = SpreeCmCommissioner::Transit::RouteTripCountDecrementerService.call(trip: trip_like)
15
+ result = SpreeCmCommissioner::Routes::DecrementTripCount.call(trip: trip_like)
16
16
  return result if result.failure?
17
17
  else
18
18
  previous_route.with_lock do
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class RouteTripCountDecrementerService
2
+ module Routes
3
+ class DecrementTripCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  def call(trip:)
@@ -1,12 +1,12 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class RouteFulfilledOrderCountIncrementerService
2
+ module Routes
3
+ class IncrementFulfilledOrderCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  def call(order:)
7
7
  return failure(nil, 'Order not found') unless order
8
8
 
9
- SpreeCmCommissioner::Transit::BaseRouteOrderMetricsUpdater
9
+ SpreeCmCommissioner::Routes::BaseUpdateOrderMetrics
10
10
  .call(order: order, attribute: :fulfilled_order_count)
11
11
 
12
12
  success(order: order)
@@ -1,12 +1,12 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class RouteOrderCountIncrementerService
2
+ module Routes
3
+ class IncrementOrderCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  def call(order:)
7
7
  return failure(nil, 'Order not found') unless order
8
8
 
9
- SpreeCmCommissioner::Transit::BaseRouteOrderMetricsUpdater
9
+ SpreeCmCommissioner::Routes::BaseUpdateOrderMetrics
10
10
  .new(order: order, attribute: :order_count)
11
11
  .call
12
12
 
@@ -1,6 +1,6 @@
1
1
  module SpreeCmCommissioner
2
- module Transit
3
- class RouteTripCountIncrementerService
2
+ module Routes
3
+ class IncrementTripCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
6
  def call(trip:)
@@ -0,0 +1,65 @@
1
+ module SpreeCmCommissioner
2
+ module Trips
3
+ class Search
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ REFRESH_RATE_LIMIT_INTERVAL = 30.seconds
7
+ SEARCH_CACHE_TTL = 5.minutes
8
+
9
+ def self.cache_key_for(params, vendor)
10
+ new.send(:cache_key, params, vendor)
11
+ end
12
+
13
+ def call(params:, vendor:, is_intercity_taxi: false)
14
+ return success(results: []) unless valid_search?(params)
15
+
16
+ cache_key = cache_key(params, vendor)
17
+
18
+ results = Rails.cache.fetch(cache_key, expires_in: SEARCH_CACHE_TTL) do
19
+ trips = SpreeCmCommissioner::TripQuery.new(
20
+ origin_id: origin_id(params),
21
+ destination_id: destination_id(params),
22
+ vendor_id: vendor.id,
23
+ number_of_guests: params[:number_of_guests] || 1,
24
+ date: search_date(params).to_date
25
+ ).call
26
+
27
+ is_intercity_taxi || taxi?(params) ? filter_intercity(trips) : trips
28
+ end
29
+
30
+ success(results: results)
31
+ rescue StandardError => e
32
+ failure(nil, e.message)
33
+ end
34
+
35
+ private
36
+
37
+ def valid_search?(params)
38
+ params[:origin].present? && params[:destination].present? && params[:outbound_date].present?
39
+ end
40
+
41
+ def filter_intercity(results)
42
+ Array(results).select do |res|
43
+ res.respond_to?(:trips) && res.trips.any? { |t| t.route_type.to_s == 'intercity_taxi' }
44
+ end
45
+ end
46
+
47
+ def origin_id(params) = outbound?(params) ? params[:origin] : params[:destination]
48
+ def destination_id(params) = outbound?(params) ? params[:destination] : params[:origin]
49
+ def search_date(params) = outbound?(params) ? params[:outbound_date] : params[:inbound_date]
50
+ def outbound?(params) = params[:direction] == 'outbound' || params[:direction].blank?
51
+ def taxi?(params) = params[:service_type] == 'intercity_taxi'
52
+
53
+ def cache_key(params, vendor)
54
+ [
55
+ 'search_trips',
56
+ "vendor:#{vendor.id}",
57
+ "dir:#{params[:direction] || 'outbound'}",
58
+ "o:#{origin_id(params)}",
59
+ "d:#{destination_id(params)}",
60
+ "date:#{search_date(params)}"
61
+ ].join(':')
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,7 +1,7 @@
1
1
  <div class="topbar">
2
2
  <div class="row" style="padding-top: 13px; margin: 0;">
3
3
  <div class="col-sm-9"></div>
4
- <div class="col-sm-3 text-right">
4
+ <div class="text-right col-sm-3">
5
5
  <%= render partial: 'blazer/queries/embed/download_button' %>
6
6
  </div>
7
7
  </div>
@@ -12,9 +12,58 @@
12
12
  <div id="results" style="font-family: 'Poppins', sans-serif;"></div>
13
13
 
14
14
  <script>
15
+ // Only apply translations for Khmer locale
16
+ <% if I18n.locale == :km %>
17
+ // Column translations from Rails i18n
18
+ const columnTranslations = {
19
+ <% I18n.t('blazer.columns', locale: I18n.locale).each do |key, value| %>
20
+ '<%= key %>': '<%= value %>',
21
+ <% end %>
22
+ };
23
+
24
+ // Function to translate table headers
25
+ function translateTableHeaders() {
26
+ const tables = document.querySelectorAll('#results table');
27
+ tables.forEach(function(table) {
28
+ const headers = table.querySelectorAll('thead th');
29
+ headers.forEach(function(header) {
30
+ const originalText = header.textContent.trim();
31
+ if (columnTranslations[originalText]) {
32
+ header.textContent = columnTranslations[originalText];
33
+ }
34
+ });
35
+ });
36
+ }
37
+ <% end %>
38
+
15
39
  function showRun(data) {
16
40
  $("#results").html(data)
17
- $("#results table").stupidtable(stupidtableCustomSettings).stickyTableHeaders({fixedOffset: 60})
41
+
42
+ // Apply table functionality
43
+ $("#results table").stupidtable(stupidtableCustomSettings)
44
+
45
+ <% if I18n.locale == :km %>
46
+ // Translate headers first, then apply sticky headers (Khmer only)
47
+ function waitForTable(callback) {
48
+ const checkTable = () => {
49
+ const table = document.querySelector('#results table');
50
+ if (table) {
51
+ callback();
52
+ } else {
53
+ setTimeout(checkTable, 50);
54
+ }
55
+ };
56
+ checkTable();
57
+ }
58
+ waitForTable(function() {
59
+ translateTableHeaders();
60
+ // Apply sticky headers after translation to ensure proper width calculation
61
+ $("#results table").stickyTableHeaders({fixedOffset: 60});
62
+ });
63
+ <% else %>
64
+ // Default behavior for English and other locales
65
+ $("#results table").stickyTableHeaders({fixedOffset: 60});
66
+ <% end %>
18
67
  }
19
68
 
20
69
  function showError(message) {
@@ -0,0 +1,21 @@
1
+ <div class="input-group">
2
+ <%= f.text_field :name, class: "form-control color-picker-input", placeholder: "eg. #FF5733", value: f.object.name, required: true, pattern: "^#[0-9A-Fa-f]{6}$", data: { target: "color-preview-#{f.object_name}" } %>
3
+ <span class="input-group-text p-0 color-preview" id="color-preview-<%= f.object_name %>" style="background-color: <%= f.object.name || '#ffffff' %>; border: 1px solid #dee2e6; border-left: none; width: 45px; border-radius: 0 0.375rem 0.375rem 0;"></span>
4
+ </div>
5
+
6
+ <script>
7
+ document.addEventListener('DOMContentLoaded', function() {
8
+ const colorInput = document.querySelector('[data-target="color-preview-<%= f.object_name %>"]');
9
+ const colorPreview = document.getElementById('color-preview-<%= f.object_name %>');
10
+
11
+ if (colorInput && colorPreview) {
12
+ colorInput.addEventListener('input', function() {
13
+ const color = this.value || '#ffffff';
14
+ // Validate hex color format
15
+ if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
16
+ colorPreview.style.backgroundColor = color;
17
+ }
18
+ });
19
+ }
20
+ });
21
+ </script>
@@ -31,6 +31,8 @@
31
31
  <td class="name"><%= f.select :name, ['post-paid', 'pre-paid'], { include_blank: true } , class: "form-control fullwidth", required: true %></td>
32
32
  <% when "delivery_option" %>
33
33
  <td class="name"><%= f.select :name, [:delivery, :pickup], { include_blank: true } , class: "form-control fullwidth", required: true %></td>
34
+ <% when "color" %>
35
+ <td class="name"><%= render partial: 'color_field', locals: { f: f } %></td>
34
36
  <% end %>
35
37
 
36
38
  <td class="presentation"><%= f.text_field :presentation, class: "form-control", required: true %></td>
@@ -0,0 +1,28 @@
1
+ <div class="input-group">
2
+ <div class="input-group-prepend">
3
+ <span class="input-group-text p-0 color-preview" id="color-preview-<%= f.object_name %>" style="background-color: <%= f.object.name || '#ffffff' %>; border: 1px solid #dee2e6; width: 45px;"></span>
4
+ </div>
5
+ <%= f.text_field :name, class: "form-control color-picker-input", placeholder: "eg. #FF5733", value: f.object.name, pattern: "^#[0-9A-Fa-f]{6}$", data: { target: "color-preview-#{f.object_name}" } %>
6
+ <div class="input-group-append">
7
+ <span class="input-group-text bg-light" style="cursor: pointer;" onclick="const input = this.closest('.input-group').querySelector('.color-picker-input'); input.value = ''; this.closest('.input-group').querySelector('.color-preview').style.backgroundColor = '#ffffff';" title="Click to clear">
8
+ <%= svg_icon name: "remove.svg", width: '14', height: '14' %>
9
+ </span>
10
+ </div>
11
+ </div>
12
+
13
+ <script>
14
+ document.addEventListener('DOMContentLoaded', function() {
15
+ const colorInput = document.querySelector('[data-target="color-preview-<%= f.object_name %>"]');
16
+ const colorPreview = document.getElementById('color-preview-<%= f.object_name %>');
17
+
18
+ if (colorInput && colorPreview) {
19
+ colorInput.addEventListener('input', function() {
20
+ const color = this.value || '#ffffff';
21
+ // Validate hex color format
22
+ if (/^#[0-9A-Fa-f]{6}$/.test(color)) {
23
+ colorPreview.style.backgroundColor = color;
24
+ }
25
+ });
26
+ }
27
+ });
28
+ </script>
@@ -1 +1,11 @@
1
- <%= f.date_field :name, value: f.object.date, class: 'rounded-left form-control bg-transparent' %>
1
+ <div class="input-group">
2
+ <div class="input-group-prepend">
3
+ <span class="input-group-text"><%= svg_icon name: "calendar.svg", width: '14', height: '14' %></span>
4
+ </div>
5
+ <%= f.date_field :name, value: f.object.date, class: 'form-control bg-transparent date-input' %>
6
+ <div class="input-group-append">
7
+ <span class="input-group-text bg-light" style="cursor: pointer;" onclick="this.closest('.input-group').querySelector('.date-input').value = '';" title="Click to clear">
8
+ <%= svg_icon name: "remove.svg", width: '14', height: '14' %>
9
+ </span>
10
+ </div>
11
+ </div>
@@ -1,5 +1,9 @@
1
1
  <% option_type = f.object.option_type %>
2
2
 
3
- <%= f.collection_select :name, option_type.option_values, :name, :presentation,
4
- { include_blank: true },
5
- { class: 'select2-clear', id: "option_value_ids-#{option_type.id}" } %>
3
+ <% if option_type.ticket_type? %>
4
+ <%= f.text_field :name, class: 'form-control' %>
5
+ <% else %>
6
+ <%= f.collection_select :name, option_type.option_values, :name, :presentation,
7
+ { include_blank: true },
8
+ { class: 'select2-clear', id: "option_value_ids-#{option_type.id}" } %>
9
+ <% end %>
@@ -1,4 +1,6 @@
1
- <%= f.fields_for :option_values do |ff| %>
1
+ <% sorted_option_values = f.object.option_values.sort_by { |ov| ov.option_type&.position || 0 } %>
2
+
3
+ <%= f.fields_for :option_values, sorted_option_values do |ff| %>
2
4
  <% option_type = ff.object.option_type %>
3
5
 
4
6
  <div class="form-group" data-hook="presentation">
@@ -28,6 +30,8 @@
28
30
  <%= render partial: 'default_field', locals: { f: ff } %>
29
31
  <% when "delivery_option" %>
30
32
  <%= render partial: 'default_field', locals: { f: ff } %>
33
+ <% when "color" %>
34
+ <%= render partial: 'color_field', locals: { f: ff } %>
31
35
  <% else %>
32
36
  <%= render partial: 'default_field', locals: { f: ff } %>
33
37
  <% end %>
@@ -2,5 +2,11 @@
2
2
  <div class="input-group-prepend">
3
3
  <span class="input-group-text"><%= svg_icon name: "clock.svg", width: '14', height: '14' %></span>
4
4
  </div>
5
- <%= time_select f.object_name, "name[time]", { ampm: true, time_separator: "", minute_step: 5, default: { hour: f.object.time&.hour, minute: f.object.time&.min } }, { class: 'form-control' } %>
5
+ <% time_value = f.object.time.present? ? f.object.time.strftime('%H:%M') : nil %>
6
+ <%= f.time_field :name, value: time_value, class: 'form-control' %>
7
+ <div class="input-group-append">
8
+ <span class="input-group-text bg-light" style="cursor: pointer;" onclick="this.parentElement.parentElement.querySelector('input[type=time]').value = '';" title="Click to clear">
9
+ <%= svg_icon name: "remove.svg", width: '14', height: '14' %>
10
+ </span>
11
+ </div>
6
12
  </div>
@@ -0,0 +1,7 @@
1
+ <tr class="adjustment">
2
+ <td><%= adjustment.label %></td>
3
+ <td class="text-right">
4
+ <%= raw(Spree::Money.new(adjustment.amount, currency: adjustment.order.currency).to_html) %>
5
+ </td>
6
+ </tr>
7
+
@@ -1,3 +1,9 @@
1
- <div class="banner">
2
- <%= image_tag(main_app.rails_blob_url(@event.home_banner.attachment), class: "banner-image", alt: t('mail.event_engagement_mailer.organizer.header_title')) %>
3
- </div>
1
+ <% if @event&.home_banner&.attachment.present? %>
2
+ <div class="banner">
3
+ <%= image_tag(
4
+ main_app.rails_blob_url(@event.home_banner.attachment),
5
+ class: "banner-image",
6
+ alt: t('mail.event_engagement_mailer.organizer.header_title')
7
+ ) %>
8
+ </div>
9
+ <% end %>