spree_cm_commissioner 2.5.14 → 2.5.15

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/concerns/spree/admin/service_calendars_concern.rb +93 -0
  4. data/app/controllers/spree/admin/product_service_calendars_controller.rb +48 -0
  5. data/app/controllers/spree/admin/sprite_import_controller.rb +84 -0
  6. data/app/controllers/spree/admin/taxons_controller_decorator.rb +6 -0
  7. data/app/controllers/spree/admin/vendor_service_calendars_controller.rb +7 -68
  8. data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +2 -0
  9. data/app/helpers/spree_cm_commissioner/admin/service_calendars_helper.rb +8 -1
  10. data/app/models/concerns/spree_cm_commissioner/homepage_section_bitwise.rb +2 -1
  11. data/app/models/concerns/spree_cm_commissioner/line_item_open_dated_trippable.rb +32 -0
  12. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +15 -2
  13. data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -0
  14. data/app/models/spree_cm_commissioner/menu_decorator.rb +33 -0
  15. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  16. data/app/overrides/spree/admin/menus/edit/add_import_sprite_button.html.erb.deface +94 -0
  17. data/app/overrides/spree/admin/shared/_product_tabs/service_calendars.html.erb.deface +8 -0
  18. data/app/serializers/spree_cm_commissioner/v2/storefront/menu_serializer_decorator.rb +32 -0
  19. data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +10 -4
  20. data/app/services/spree_cm_commissioner/check_ins/create_bulk.rb +2 -2
  21. data/app/services/spree_cm_commissioner/sprite_data_loader_service.rb +46 -0
  22. data/app/services/spree_cm_commissioner/sprite_import_service.rb +101 -0
  23. data/app/services/spree_cm_commissioner/transit_order/create.rb +32 -15
  24. data/app/services/spree_cm_commissioner/trips/create_open_dated_trip.rb +0 -1
  25. data/app/views/spree/admin/homepage_background/index.html.erb +1 -0
  26. data/app/views/spree/admin/product_service_calendars/index.html.erb +70 -0
  27. data/app/views/spree/admin/product_service_calendars/new.html.erb +9 -0
  28. data/app/views/spree/admin/{vendor_service_calendars → shared/service_calendars}/_form.html.erb +1 -1
  29. data/app/views/spree/admin/sprite_import/_auto_fill_script.html.erb +27 -0
  30. data/app/views/spree/admin/sprite_import/_positions_table.html.erb +42 -0
  31. data/app/views/spree/admin/sprite_import/edit.html.erb +72 -0
  32. data/app/views/spree/admin/sprite_import/new.html.erb +72 -0
  33. data/app/views/spree/admin/sprite_import/show.html.erb +194 -0
  34. data/app/views/spree/admin/taxon_childrens/index.html.erb +4 -0
  35. data/app/views/spree/admin/vendor_service_calendars/new.html.erb +2 -2
  36. data/config/locales/en.yml +2 -0
  37. data/config/routes.rb +11 -1
  38. data/db/migrate/20260220041100_add_public_metadata_to_spree_menus.rb +6 -0
  39. data/docs/sprite_icon_implementation_prompt.md +717 -0
  40. data/docs/sprite_import_implementation_guide.md +181 -0
  41. data/lib/spree_cm_commissioner/version.rb +1 -1
  42. metadata +24 -4
  43. /data/app/views/spree/admin/{vendor_service_calendars → shared/service_calendars}/_exception_rules.html.erb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 574ed1a0c32a052dce78787ecfa40603a68f033ff8a113f3ae7a91e415656fe6
4
- data.tar.gz: 3640a360d2df37c723529f091136e1491c2a0cf61515993d6ebd502e01b3996f
3
+ metadata.gz: e1489014b8629a83035954b981e4b2e7cdfdcb70159c2e871e9152c3916668ae
4
+ data.tar.gz: ee0ec5e1f58355695e006f00d7952a09ca379283eab18591bba435d1c535fffb
5
5
  SHA512:
6
- metadata.gz: 893ac42ed0c4c306260bd8b578fed2619d7ee65daf13d009228f81ae2047a98e164983b89b0345344d384cffe1f88989c20b717685e40a38c5abf8a159609e77
7
- data.tar.gz: 5d145d664bbba939d59f2a5797ff96494993c911b2dba2159a23c8accafce6bc0e9719b7e62697f1bebd2f2ef7151544031a47f9993eb8311c745ef84c76283d
6
+ metadata.gz: 1f3cafbb00e42a98ec6f80ad52266e476cde61f2d4d6215f80cdc95c7af604c5f754aea35139331a8dcf98ccbc25f43378d94c815732cbc0fa8185ae8373e58a
7
+ data.tar.gz: 6562e8994dde887a43e598a29239ca6e8295967db6bfc0d8fa506ec56a69097ad293f1f83355c8db6db671ad433d3cc07f7009e67688450418c868e9c9de3814
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.14)
37
+ spree_cm_commissioner (2.5.15)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,93 @@
1
+ module Spree
2
+ module Admin
3
+ module ServiceCalendarsConcern
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ new_action.before :set_exception_rules
8
+
9
+ helper 'spree_cm_commissioner/admin/service_calendars'
10
+ end
11
+
12
+ def update_status
13
+ if @object.update(active: !@object.active)
14
+ flash[:success] = flash_message_for(@object, :successfully_updated)
15
+ else
16
+ flash[:error] = @object.errors.full_messages.to_sentence
17
+ end
18
+
19
+ redirect_to collection_url
20
+ rescue ActiveRecord::RecordInvalid => e
21
+ flash[:error] = e.message
22
+ redirect_to collection_url
23
+ end
24
+
25
+ protected
26
+
27
+ def model_class
28
+ SpreeCmCommissioner::ServiceCalendar
29
+ end
30
+
31
+ def object_name
32
+ 'spree_cm_commissioner_service_calendars'
33
+ end
34
+
35
+ def permitted_resource_params
36
+ service_calendar_params = params.require(:spree_cm_commissioner_service_calendar)
37
+ .permit(:calendarable_id, :calendarable_type, :start_date, :end_date,
38
+ :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
39
+ )
40
+ service_calendar_params[:exception_rules] = build_exception_rules(params[:spree_cm_commissioner_service_calendar][:exception_rules])
41
+ service_calendar_params
42
+ end
43
+
44
+ private
45
+
46
+ def build_exception_rules(exception_rules)
47
+ exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? || rule['type'].blank? } || exception_rules.values
48
+ end
49
+
50
+ def collection
51
+ load_calendarable
52
+
53
+ @objects = model_class.where(
54
+ calendarable_type: calendarable_type,
55
+ calendarable_id: calendarable_id
56
+ ).order(id: :desc)
57
+ end
58
+
59
+ def set_exception_rules
60
+ @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
61
+ end
62
+
63
+ def build_resource
64
+ today = Time.zone.today
65
+ model_class.new(start_date: today, end_date: today.next_year(3),
66
+ exception_rules: [{ from: nil, to: nil, type: 'inclusion' }]
67
+ )
68
+ end
69
+
70
+ def set_calendarable
71
+ @object.calendarable_type = calendarable_type
72
+ @object.calendarable_id = calendarable_id
73
+ end
74
+
75
+ # To be implemented by including controllers
76
+ def load_calendarable
77
+ raise NotImplementedError, 'must implement load_calendarable'
78
+ end
79
+
80
+ def calendarable
81
+ raise NotImplementedError, 'must implement calendarable'
82
+ end
83
+
84
+ def calendarable_id
85
+ calendarable.id
86
+ end
87
+
88
+ def calendarable_type
89
+ calendarable.class.name
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,48 @@
1
+ module Spree
2
+ module Admin
3
+ class ProductServiceCalendarsController < Spree::Admin::ResourceController
4
+ include Spree::Admin::ServiceCalendarsConcern
5
+
6
+ before_action :load_product
7
+ before_action :ensure_product_type_supports_calendar
8
+ before_action :ensure_product_has_no_calendar, only: %i[new create]
9
+
10
+ create.before :set_calendarable
11
+ update.before :set_calendarable
12
+
13
+ protected
14
+
15
+ def collection_url(options = {})
16
+ admin_product_product_service_calendars_url(options)
17
+ end
18
+
19
+ private
20
+
21
+ def load_product
22
+ @product ||= (Spree::Product.find_by(slug: params[:product_id]) || Spree::Product.find_by(id: params[:product_id]))
23
+ end
24
+
25
+ def ensure_product_type_supports_calendar
26
+ return if @product.permanent_stock?
27
+
28
+ flash[:error] = I18n.t('vendor.service_calendars.not_available_for_product_type')
29
+ redirect_to edit_admin_product_url(@product)
30
+ end
31
+
32
+ def ensure_product_has_no_calendar
33
+ return if @product.service_calendar.blank?
34
+
35
+ flash[:error] = I18n.t('vendor.service_calendars.already_has_calendar')
36
+ redirect_to admin_product_product_service_calendars_url(@product)
37
+ end
38
+
39
+ def load_calendarable
40
+ load_product
41
+ end
42
+
43
+ def calendarable
44
+ @product
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,84 @@
1
+ module Spree
2
+ module Admin
3
+ class SpriteImportController < BaseController
4
+ before_action :load_menu
5
+ before_action :reload_menu_with_attachment, only: [:show]
6
+ before_action :load_sprite_data, only: %i[show new edit create update]
7
+
8
+ def show; end
9
+
10
+ def new; end
11
+
12
+ def edit; end
13
+
14
+ def create
15
+ result = sprite_import_service.call
16
+ handle_service_result(result, :new)
17
+ end
18
+
19
+ def update
20
+ result = sprite_import_service.update
21
+ handle_service_result(result, :edit)
22
+ end
23
+
24
+ def destroy
25
+ @menu.sprite_file.purge if @menu.sprite_file.attached?
26
+ @menu.sprite_positions = {}
27
+ @menu.sprite_padding = 16
28
+ @menu.sprite_gap = 16
29
+ @menu.sprite_icon_size = 64
30
+ @menu.save!
31
+ flash[:success] = 'Sprite removed successfully!' # rubocop:disable Rails/I18nLocaleTexts
32
+ redirect_to spree.admin_menu_sprite_import_path(@menu)
33
+ end
34
+
35
+ private
36
+
37
+ def load_menu
38
+ @menu = Spree::Menu.find(params[:menu_id])
39
+ end
40
+
41
+ def reload_menu_with_attachment
42
+ data_loader = sprite_data_loader_service
43
+ result = data_loader.reload_menu_with_attachment
44
+ @menu = result[:menu]
45
+ @sprite_url = result[:sprite_url]
46
+ end
47
+
48
+ def load_sprite_data
49
+ data_loader = sprite_data_loader_service
50
+ sprite_data = data_loader.load_sprite_data
51
+ @menu_items = sprite_data[:menu_items]
52
+ @existing_positions = sprite_data[:existing_positions]
53
+ @sprite_url = sprite_data[:sprite_url] if sprite_data[:sprite_url]
54
+ end
55
+
56
+ def sprite_import_service
57
+ SpreeCmCommissioner::SpriteImportService.new(
58
+ menu: @menu,
59
+ sprite_file: params[:sprite_file],
60
+ sprite_settings: {
61
+ padding: params[:sprite_padding],
62
+ gap: params[:sprite_gap],
63
+ icon_size: params[:sprite_icon_size]
64
+ },
65
+ positions_params: params[:positions]
66
+ )
67
+ end
68
+
69
+ def sprite_data_loader_service
70
+ SpreeCmCommissioner::SpriteDataLoaderService.new(@menu)
71
+ end
72
+
73
+ def handle_service_result(result, render_action)
74
+ if result.success?
75
+ flash[:success] = result.value[:message]
76
+ redirect_to spree.admin_menu_sprite_import_path(@menu)
77
+ else
78
+ flash[:error] = result.value
79
+ render render_action
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -5,6 +5,12 @@ module Spree
5
5
  base.before_action :build_assets, only: %i[create update]
6
6
  end
7
7
 
8
+ # override
9
+ def new
10
+ @taxon.parent_id = params[:parent_id] if params[:parent_id].present?
11
+ super
12
+ end
13
+
8
14
  def remove_category_icon
9
15
  remove_asset(@taxon.category_icon)
10
16
  end
@@ -1,92 +1,31 @@
1
1
  module Spree
2
2
  module Admin
3
3
  class VendorServiceCalendarsController < Spree::Admin::ResourceController
4
- before_action :load_vendor
5
- new_action.before :set_exception_rules
6
-
7
- create.before :set_vendor
8
- update.before :set_vendor
9
-
10
- helper 'spree_cm_commissioner/admin/service_calendars'
4
+ include Spree::Admin::ServiceCalendarsConcern
11
5
 
12
- def update_status
13
- if @object.update(active: !@object.active)
14
- flash[:success] = flash_message_for(@object, :successfully_updated)
15
- else
16
- flash[:error] = @object.errors.full_messages.to_sentence
17
- end
6
+ before_action :load_vendor
18
7
 
19
- redirect_to admin_vendor_vendor_service_calendars_url
20
- rescue ActiveRecord::RecordInvalid => e
21
- flash[:error] = e.message
22
- redirect_to admin_vendor_vendor_service_calendars_url
23
- end
8
+ create.before :set_calendarable
9
+ update.before :set_calendarable
24
10
 
25
11
  protected
26
12
 
27
- def model_class
28
- SpreeCmCommissioner::ServiceCalendar
29
- end
30
-
31
- def object_name
32
- 'spree_cm_commissioner_service_calendars'
33
- end
34
-
35
13
  def collection_url(options = {})
36
14
  admin_vendor_vendor_service_calendars_url(options)
37
15
  end
38
16
 
39
- def permitted_resource_params
40
- service_calendar_params = params.require(:spree_cm_commissioner_service_calendar)
41
- .permit(:calendarable_id, :calendarable_type, :start_date, :end_date,
42
- :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, :sunday
43
- )
44
- service_calendar_params[:exception_rules] = build_exception_rules(params[:spree_cm_commissioner_service_calendar][:exception_rules])
45
- service_calendar_params
46
- end
47
-
48
17
  private
49
18
 
50
19
  def load_vendor
51
20
  @vendor ||= (Spree::Vendor.find_by(slug: params[:vendor_id]) || Spree::Vendor.find_by(id: params[:vendor_id]))
52
21
  end
53
22
 
54
- def set_vendor
55
- @object.calendarable_type = calendarable_type
56
- @object.calendarable_id = calendarable_id
57
- end
58
-
59
- def build_exception_rules(exception_rules)
60
- exception_rules.values.reject! { |rule| rule['from'].blank? || rule['to'].blank? || rule['type'].blank? } || exception_rules.values
61
- end
62
-
63
- def collection
23
+ def load_calendarable
64
24
  load_vendor
65
-
66
- @objects = model_class.where(
67
- calendarable_type: calendarable_type,
68
- calendarable_id: calendarable_id
69
- ).order(id: :desc)
70
- end
71
-
72
- def set_exception_rules
73
- @exception_rules = [{ from: DateTime.now, to: DateTime.now, type: 'exclusion', reason: nil }]
74
- end
75
-
76
- # it load before :load_vendor
77
- def build_resource
78
- today = Time.zone.today
79
- model_class.new(start_date: today, end_date: today.next_year(3),
80
- exception_rules: [{ from: nil, to: nil, type: 'inclusion' }]
81
- )
82
- end
83
-
84
- def calendarable_id
85
- @vendor.id
86
25
  end
87
26
 
88
- def calendarable_type
89
- @vendor.class.name
27
+ def calendarable
28
+ @vendor
90
29
  end
91
30
  end
92
31
  end
@@ -13,6 +13,8 @@ module SpreeCmCommissioner
13
13
  'badge badge-warning text-uppercase'
14
14
  when :things_to_do
15
15
  'badge badge-success text-uppercase'
16
+ when :football
17
+ 'badge badge-dark text-uppercase'
16
18
  else
17
19
  'badge'
18
20
  end
@@ -5,9 +5,16 @@ module SpreeCmCommissioner
5
5
  label = resource.active ? 'Active' : 'Disabled'
6
6
  btn_active_class = resource.active ? 'btn-primary' : 'btn-warning'
7
7
 
8
+ # Determine the correct URL based on the calendarable type
9
+ url = if resource.calendarable_type == 'Spree::Product'
10
+ update_status_admin_product_product_service_calendar_path(resource.calendarable, resource)
11
+ else
12
+ update_status_admin_vendor_vendor_service_calendar_url(resource.calendarable, resource)
13
+ end
14
+
8
15
  button_to(
9
16
  label,
10
- update_status_admin_vendor_vendor_service_calendar_url(resource.calendarable, resource),
17
+ url,
11
18
  form: { data: { confirm: 'Are you sure?' }, class: "btn btn-sm btn-active #{btn_active_class}" },
12
19
  method: :patch
13
20
  )
@@ -7,7 +7,8 @@ module SpreeCmCommissioner
7
7
  ticket: 0b00010,
8
8
  tour: 0b00100,
9
9
  accommodation: 0b01000,
10
- things_to_do: 0b10000
10
+ things_to_do: 0b10000,
11
+ football: 0b100000
11
12
  }.freeze
12
13
 
13
14
  BIT_SEGMENT.each do |segment, bit_value|
@@ -0,0 +1,32 @@
1
+ module SpreeCmCommissioner
2
+ module LineItemOpenDatedTrippable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include SpreeCmCommissioner::StoreMetadata
7
+
8
+ # Open dated trip metadata
9
+ store_public_metadata :is_open_dated, :boolean, default: false
10
+ store_public_metadata :redeemed_trip_id, :integer # Which trip chosen for redemption
11
+
12
+ # Alias existing columns for better semantic meaning
13
+ alias_attribute :valid_until, :to_date # Expiration date
14
+ alias_attribute :redeemed_at, :accepted_at # Redemption timestamp
15
+ end
16
+
17
+ # Check if ticket has been redeemed
18
+ def redeemed?
19
+ redeemed_at.present?
20
+ end
21
+
22
+ # Check if ticket is expired
23
+ def expired?
24
+ valid_until.present? && valid_until.to_date < Date.current
25
+ end
26
+
27
+ # Check if ticket can be redeemed
28
+ def can_redeem?
29
+ is_open_dated? && !redeemed? && !expired?
30
+ end
31
+ end
32
+ end
@@ -25,10 +25,10 @@ module SpreeCmCommissioner
25
25
  included do
26
26
  validates :direction, inclusion: { in: DIRECTION }, allow_nil: true
27
27
  validates :trip_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
28
- validates :boarding_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
29
- validates :drop_off_trip_stop_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
30
28
  validates :service_origin_id, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
31
29
 
30
+ validate :validate_trip_stops_for_scheduled_trips
31
+
32
32
  validates :passenger_count, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
33
33
 
34
34
  # Groups LineItems by trip fields. Some fields can be null,
@@ -125,6 +125,19 @@ module SpreeCmCommissioner
125
125
 
126
126
  private
127
127
 
128
+ def validate_trip_stops_for_scheduled_trips
129
+ return if trip_id.blank? || trip_id.zero?
130
+
131
+ trip = SpreeCmCommissioner::Trip.find_by(id: trip_id)
132
+ return unless trip # Skip if trip doesn't exist (will be caught by other validations)
133
+ return if trip.open_dated? # Skip validation for open-dated trips
134
+
135
+ # For scheduled trips, require valid trip stops
136
+ errors.add(:boarding_trip_stop_id, 'must be greater than 0') if boarding_trip_stop_id.present? && boarding_trip_stop_id <= 0
137
+
138
+ errors.add(:drop_off_trip_stop_id, 'must be greater than 0') if drop_off_trip_stop_id.present? && drop_off_trip_stop_id <= 0
139
+ end
140
+
128
141
  def set_public_metadata_object(key, value)
129
142
  if value.nil?
130
143
  public_metadata.delete(key)
@@ -80,6 +80,7 @@ module SpreeCmCommissioner
80
80
  base.include SpreeCmCommissioner::Integrations::IntegrationMappable
81
81
  base.include SpreeCmCommissioner::ProductType
82
82
  base.include SpreeCmCommissioner::ProductDelegation
83
+ base.include SpreeCmCommissioner::LineItemOpenDatedTrippable
83
84
  base.include SpreeCmCommissioner::KycBitwise
84
85
  end
85
86
 
@@ -0,0 +1,33 @@
1
+ module SpreeCmCommissioner
2
+ module MenuDecorator
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include SpreeCmCommissioner::StoreMetadata
7
+
8
+ store_public_metadata :sprite_positions, :hash, default: {}
9
+ store_public_metadata :sprite_padding, :integer, default: 16
10
+ store_public_metadata :sprite_gap, :integer, default: 16
11
+ store_public_metadata :sprite_icon_size, :integer, default: 64
12
+
13
+ has_one_attached :sprite_file
14
+ end
15
+
16
+ def get_sprite_position(menu_item_id)
17
+ pos = sprite_positions[menu_item_id.to_s] || { 'x' => 0, 'y' => 0, 'width' => 64, 'height' => 64 }
18
+ pos.with_indifferent_access
19
+ end
20
+
21
+ def update_sprite_positions(positions_hash)
22
+ self.sprite_positions = positions_hash
23
+ end
24
+
25
+ def sprite_url
26
+ return nil unless sprite_file.attached?
27
+
28
+ Rails.application.routes.url_helpers.cdn_image_url(sprite_file)
29
+ end
30
+ end
31
+ end
32
+
33
+ Spree::Menu.include SpreeCmCommissioner::MenuDecorator unless Spree::Menu.included_modules.include?(SpreeCmCommissioner::MenuDecorator)
@@ -15,6 +15,7 @@ module SpreeCmCommissioner
15
15
  base.after_commit :increment_route_order_count, on: :create
16
16
 
17
17
  base.store_private_metadata :preload_trip_ids, :array, default: []
18
+ base.store_public_metadata :order_options, :array, default: []
18
19
 
19
20
  base.validates :promo_total, base::MONEY_VALIDATION
20
21
  base.validate :validate_channel_prefix, if: :channel_changed?
@@ -0,0 +1,94 @@
1
+ <!-- insert_before "h5:contains('settings')" -->
2
+ <%
3
+ sprite_attached = @menu.respond_to?(:sprite_file) && @menu.sprite_file.attached?
4
+ sprite_url = @menu.respond_to?(:sprite_url) ? @menu.sprite_url : nil
5
+ %>
6
+
7
+ <div data-hook="sprite_management" class="card mb-3">
8
+ <div class="card-header">
9
+ <div class="row">
10
+ <div class="col d-flex">
11
+ <h5 class="mb-0 align-self-center">Sprite Management</h5>
12
+ </div>
13
+ <div class="col d-flex justify-content-end">
14
+ <% if sprite_attached %>
15
+ <%= link_to spree.admin_menu_sprite_import_path(@menu), class: 'btn btn-outline-secondary align-self-center mr-2' do %>
16
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
17
+ View Sprite
18
+ <% end %>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <div class="card-body">
25
+ <% if sprite_attached %>
26
+ <div class="row">
27
+ <div class="col-md-7">
28
+ <div class="card bg-light border-0">
29
+ <div class="card-body py-3">
30
+ <div class="row">
31
+ <div class="col-sm-6">
32
+ <dl class="mb-0">
33
+ <dt class="text-muted small text-uppercase">File</dt>
34
+ <dd class="mb-2"><%= @menu.sprite_file.filename %></dd>
35
+
36
+ <dt class="text-muted small text-uppercase">File Size</dt>
37
+ <dd class="mb-0"><%= number_to_human_size(@menu.sprite_file.byte_size) %></dd>
38
+ </dl>
39
+ </div>
40
+ <div class="col-sm-6">
41
+ <dl class="mb-0">
42
+ <dt class="text-muted small text-uppercase">Icon Size</dt>
43
+ <dd class="mb-2"><%= @menu.sprite_icon_size || 64 %> x <%= @menu.sprite_icon_size || 64 %> px</dd>
44
+
45
+ <dt class="text-muted small text-uppercase">Total Icons</dt>
46
+ <dd class="mb-0">
47
+ <span class="badge badge-info" style="font-size: 0.85em;"><%= @menu.total_icons %></span>
48
+ </dd>
49
+ </dl>
50
+ </div>
51
+ </div>
52
+ <div class="mt-2 pt-2 border-top">
53
+ <small class="text-muted">
54
+ Last updated: <%= @menu.sprite_file.created_at.strftime("%b %d, %Y at %H:%M") if @menu.sprite_file.created_at %>
55
+ </small>
56
+ </div>
57
+ <% if sprite_url %>
58
+ <div class="mt-1">
59
+ <small class="text-muted text-break" style="font-size: 0.75em; word-break: break-all;">CDN: <%= sprite_url %></small>
60
+ </div>
61
+ <% end %>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ <div class="col-md-5">
66
+ <div class="card border-0 bg-light h-100">
67
+ <div class="card-body d-flex align-items-center justify-content-center p-3">
68
+ <% if sprite_url %>
69
+ <%= image_tag sprite_url, class: "img-fluid", style: "max-height: 180px; border-radius: 4px;" %>
70
+ <% else %>
71
+ <span class="text-muted">No preview available</span>
72
+ <% end %>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <% else %>
78
+ <div class="text-center py-5">
79
+ <div class="mb-3">
80
+ <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#adb5bd" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
81
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
82
+ <circle cx="8.5" cy="8.5" r="1.5"/>
83
+ <polyline points="21 15 16 10 5 21"/>
84
+ </svg>
85
+ </div>
86
+ <h5>No Sprite Imported Yet</h5>
87
+ <p class="text-muted mb-4" style="max-width: 360px; margin: 0 auto;">
88
+ Import a sprite sheet to serve all menu icons in a single optimized image.
89
+ </p>
90
+ <%= link_to "Import Sprite", spree.new_admin_menu_sprite_import_path(@menu), class: "btn btn-primary" %>
91
+ </div>
92
+ <% end %>
93
+ </div>
94
+ </div>
@@ -0,0 +1,8 @@
1
+ <!-- insert_after "erb[loud]:contains('current == :stock')" -->
2
+
3
+ <%= content_tag :li, class: 'nav-item' do %>
4
+ <%= link_to_with_icon 'calendar.svg',
5
+ t('vendor.service_calendars.service_calendar'),
6
+ admin_product_product_service_calendars_path(@product),
7
+ class: "nav-link #{'active' if current == :service_calendars}" %>
8
+ <% end if can?(:create, SpreeCmCommissioner::ServiceCalendar) && !@product.deleted? && @product.permanent_stock? %>
@@ -0,0 +1,32 @@
1
+ module SpreeCmCommissioner
2
+ module V2
3
+ module Storefront
4
+ module MenuSerializerDecorator
5
+ def self.prepended(base)
6
+ base.attributes :sprite_url
7
+
8
+ base.attribute :icon_positions do |menu|
9
+ positions = menu.sprite_positions || {}
10
+ ordered_items = menu.menu_items.where.not(parent_id: nil).order(:lft)
11
+ ordered_items.each_with_object({}) do |item, hash|
12
+ id = item.id.to_s
13
+ next unless positions.key?(id)
14
+
15
+ pos = positions[id]
16
+ hash[id] = {
17
+ 'x' => pos['x'],
18
+ 'y' => pos['y'],
19
+ 'width' => pos['width'],
20
+ 'height' => pos['height']
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ if Spree::V2::Storefront::MenuSerializer.included_modules.exclude?(SpreeCmCommissioner::V2::Storefront::MenuSerializerDecorator)
31
+ Spree::V2::Storefront::MenuSerializer.prepend(SpreeCmCommissioner::V2::Storefront::MenuSerializerDecorator)
32
+ end