spree_cm_commissioner 2.3.0.pre.pre15 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +33 -0
- data/app/controllers/blazer/base_controller_decorator.rb +20 -1
- data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +40 -0
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +47 -9
- data/app/controllers/spree_cm_commissioner/admin/variants_controller_decorator.rb +1 -1
- data/app/factory/spree_cm_commissioner/vendor_telegram_message_factory.rb +65 -0
- data/app/helpers/spree/base_helper_decorator.rb +2 -19
- data/app/interactors/spree_cm_commissioner/vendor_creation_telegram_alert_sender.rb +28 -0
- data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +1 -1
- data/app/jobs/spree_cm_commissioner/vendor_creation_telegram_alert_sender_job.rb +10 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +51 -0
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +7 -0
- data/app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb +25 -0
- data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +18 -1
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +2 -2
- data/app/models/concerns/spree_cm_commissioner/route_order_countable.rb +3 -14
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +18 -4
- data/app/models/spree_cm_commissioner/block.rb +19 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/variant_decorator.rb +0 -5
- data/app/models/spree_cm_commissioner/variant_options.rb +8 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
- data/app/overrides/spree/admin/variants/_form/add_permanent_stock.html.erb.deface +2 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_cart_serializer.rb +12 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +31 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +11 -0
- data/app/services/spree_cm_commissioner/checkout/advance_decorator.rb +8 -4
- data/app/services/spree_cm_commissioner/intercity_taxi_order/create.rb +67 -0
- data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +79 -0
- data/app/services/spree_cm_commissioner/{transit/base_route_order_metrics_updater.rb → routes/base_update_order_metrics.rb} +2 -2
- data/app/services/spree_cm_commissioner/{transit/route_previous_trip_count_decrementer_service.rb → routes/decrement_previous_trip_count.rb} +3 -3
- data/app/services/spree_cm_commissioner/{transit/route_trip_count_decrementer_service.rb → routes/decrement_trip_count.rb} +2 -2
- data/app/services/spree_cm_commissioner/{transit/route_fulfilled_order_count_incrementer_service.rb → routes/increment_fulfilled_order_count.rb} +3 -3
- data/app/services/spree_cm_commissioner/{transit/route_order_count_incrementer_service.rb → routes/increment_order_count.rb} +3 -3
- data/app/services/spree_cm_commissioner/{transit/route_trip_count_incrementer_service.rb → routes/increment_trip_count.rb} +2 -2
- data/app/services/spree_cm_commissioner/trips/search.rb +65 -0
- data/app/views/blazer/queries/embed/_content.html.erb +51 -2
- data/app/views/spree/admin/option_types/_color_field.html.erb +21 -0
- data/app/views/spree/admin/option_types/_option_value_fields.html.erb +2 -0
- data/app/views/spree/admin/variants/_color_field.html.erb +28 -0
- data/app/views/spree/admin/variants/_date_field.html.erb +11 -1
- data/app/views/spree/admin/variants/_default_field.html.erb +7 -3
- data/app/views/spree/admin/variants/_option_values.html.erb +5 -1
- data/app/views/spree/admin/variants/_time_field.html.erb +7 -1
- data/app/views/spree/order_mailer/_adjustment.html.erb +7 -0
- data/app/views/spree_cm_commissioner/event_transactional_mailer/_event_banner.html.erb +9 -3
- data/config/locales/km.yml +64 -0
- data/config/routes.rb +4 -1
- data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +18 -0
- data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +30 -0
- data/lib/spree_cm_commissioner/trip_result.rb +15 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +20 -8
|
@@ -29,10 +29,18 @@ module SpreeCmCommissioner
|
|
|
29
29
|
:bib_pre_generation_on_create?,
|
|
30
30
|
:seat_number_positions,
|
|
31
31
|
:seat_number_layouts,
|
|
32
|
+
:color,
|
|
33
|
+
:ticket_type,
|
|
32
34
|
:seat_type,
|
|
33
35
|
to: :options
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# Override variant.rb to return cached formatted options text, avoiding repeated database queries.
|
|
39
|
+
# Falls back to database queries & computing the format if preload data is unavailable.
|
|
40
|
+
def options_text
|
|
41
|
+
@options_text ||= public_metadata[:preload_options_text].presence || Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
|
|
42
|
+
end
|
|
43
|
+
|
|
36
44
|
def options
|
|
37
45
|
@options ||= VariantOptions.new(self)
|
|
38
46
|
end
|
|
@@ -87,17 +95,23 @@ module SpreeCmCommissioner
|
|
|
87
95
|
options.payment_option == 'post-paid'
|
|
88
96
|
end
|
|
89
97
|
|
|
90
|
-
# save optins to public_metadata so we don't have to query option types & option values when needed them.
|
|
91
|
-
# once variant changed, we update metadata.
|
|
92
98
|
def set_options_to_public_metadata
|
|
93
99
|
self.public_metadata ||= {}
|
|
94
100
|
|
|
95
|
-
|
|
101
|
+
# Cache option values as a hash to avoid N+1 queries when accessing options.
|
|
102
|
+
# Stores {option_type_name => option_value_name} pairs that can be retrieved
|
|
103
|
+
# without loading option_types and option_values associations.
|
|
104
|
+
# Example: "Red, 256GB" - formatted options for quick display
|
|
105
|
+
self.public_metadata[:cm_options] = option_values.each_with_object({}) do |option_value, hash|
|
|
96
106
|
option_type_name = option_value.option_type.name
|
|
97
107
|
hash[option_type_name] = find_option_value_name_for(option_type_name: option_type_name)
|
|
98
108
|
end
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
# Cache formatted options text for quick retrieval via #options_text.
|
|
111
|
+
# Precomputes the human-readable format (e.g., "Red, 256GB") to avoid
|
|
112
|
+
# repeated formatting and association queries.
|
|
113
|
+
# Example: {"color" => "red", "storage" => "256GB"} - option_type_name => option_value_name pairs
|
|
114
|
+
self.public_metadata[:preload_options_text] = Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
|
|
101
115
|
end
|
|
102
116
|
|
|
103
117
|
def set_options_to_public_metadata!
|
|
@@ -25,6 +25,8 @@ module SpreeCmCommissioner
|
|
|
25
25
|
validates :y, presence: true, numericality: true
|
|
26
26
|
validates :rotation, presence: true, numericality: { greater_than_or_equal_to: -360, less_than_or_equal_to: 360 }
|
|
27
27
|
|
|
28
|
+
validate :cannot_unassign_variant_with_active_blocks
|
|
29
|
+
|
|
28
30
|
before_validation :assign_layout_from_section, if: -> { seat_layout.nil? && seat_section.present? }
|
|
29
31
|
|
|
30
32
|
def label_required?
|
|
@@ -50,5 +52,22 @@ module SpreeCmCommissioner
|
|
|
50
52
|
def assign_layout_from_section
|
|
51
53
|
self.seat_layout = seat_section.seat_layout
|
|
52
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def cannot_unassign_variant_with_active_blocks
|
|
59
|
+
return if variant_id_was.blank? || variant_id.present?
|
|
60
|
+
|
|
61
|
+
active_blocks = all_reserved_blocks.reserved_or_on_hold
|
|
62
|
+
return unless active_blocks.exists?
|
|
63
|
+
|
|
64
|
+
block_count = active_blocks.count
|
|
65
|
+
block_statuses = active_blocks.pluck(:status).uniq.map { |s| s.humanize.downcase }
|
|
66
|
+
|
|
67
|
+
errors.add(
|
|
68
|
+
:variant,
|
|
69
|
+
"cannot be unassigned because #{block_count} #{block_count == 1 ? 'block is' : 'blocks are'} #{block_statuses.join(' and ')} by guests"
|
|
70
|
+
)
|
|
71
|
+
end
|
|
53
72
|
end
|
|
54
73
|
end
|
|
@@ -7,6 +7,7 @@ module SpreeCmCommissioner
|
|
|
7
7
|
base.include SpreeCmCommissioner::Metafield
|
|
8
8
|
base.include SpreeCmCommissioner::TenantUpdatable
|
|
9
9
|
base.include SpreeCmCommissioner::ServiceType
|
|
10
|
+
base.include SpreeCmCommissioner::ServiceRecommendations
|
|
10
11
|
|
|
11
12
|
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
|
|
12
13
|
through: :product_option_types, source: :option_type
|
|
@@ -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
|
-
|
|
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
|
data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb
ADDED
|
@@ -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
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module Checkout
|
|
3
3
|
module AdvanceDecorator
|
|
4
|
-
# override
|
|
4
|
+
# override to capture seats error only, other errors is fine we can return success with latest order object instead of throw error.
|
|
5
|
+
# seat is special case because we want to return error message.
|
|
5
6
|
def call(order:)
|
|
6
7
|
Spree::Dependencies.checkout_next_service.constantize.call(order: order) until cannot_make_transition?(order)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
|
|
9
|
+
if order.errors[:seats].present?
|
|
10
|
+
failure(order)
|
|
11
|
+
else
|
|
12
|
+
success(order)
|
|
13
|
+
end
|
|
10
14
|
end
|
|
11
15
|
end
|
|
12
16
|
end
|
|
@@ -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
|
|
3
|
-
class
|
|
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::
|
|
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,12 +1,12 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
|
-
module
|
|
3
|
-
class
|
|
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::
|
|
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
|
|
3
|
-
class
|
|
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::
|
|
9
|
+
SpreeCmCommissioner::Routes::BaseUpdateOrderMetrics
|
|
10
10
|
.new(order: order, attribute: :order_count)
|
|
11
11
|
.call
|
|
12
12
|
|
|
@@ -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
|
|
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
|
-
|
|
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>
|