spree_boxnow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +225 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_boxnow_manifest.js +3 -0
- data/app/assets/images/integration_icons/boxnow-logo.png +0 -0
- data/app/controllers/spree/admin/boxnow_controller.rb +126 -0
- data/app/controllers/spree/boxnow_controller.rb +24 -0
- data/app/javascript/spree_boxnow/application.js +16 -0
- data/app/javascript/spree_boxnow/controllers/spree_boxnow_controller.js +34 -0
- data/app/lib/spree_boxnow/api_client.rb +108 -0
- data/app/models/spree/calculator/shipping/boxnow_rate.rb +123 -0
- data/app/models/spree/integrations/boxnow.rb +30 -0
- data/app/models/spree/order_decorator.rb +17 -0
- data/app/models/spree/shipment_decorator.rb +17 -0
- data/app/services/spree_boxnow/cancel_voucher.rb +24 -0
- data/app/services/spree_boxnow/create_voucher.rb +79 -0
- data/app/services/spree_boxnow/print_vouchers.rb +17 -0
- data/app/views/spree/admin/integrations/forms/_boxnow.html.erb +14 -0
- data/app/views/spree/admin/orders/_shipment.html.erb +198 -0
- data/app/views/spree/admin/shipping_methods/_boxnow_form.html.erb +13 -0
- data/app/views/spree/checkout/_delivery_shipping_rate.html.erb +31 -0
- data/app/views/spree_boxnow/_head.html.erb +1 -0
- data/app/views/spree_boxnow/_locker_picker.html.erb +101 -0
- data/app/views/spree_boxnow/_order_dropdown_options.html.erb +38 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/spree.rb +12 -0
- data/config/locales/el.yml +74 -0
- data/config/locales/en.yml +74 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20260210120618_add_boxnow_to_spree_shipping_methods.rb +5 -0
- data/lib/generators/spree_boxnow/install/install_generator.rb +20 -0
- data/lib/spree_boxnow/configuration.rb +13 -0
- data/lib/spree_boxnow/engine.rb +39 -0
- data/lib/spree_boxnow/factories.rb +10 -0
- data/lib/spree_boxnow/version.rb +7 -0
- data/lib/spree_boxnow.rb +11 -0
- metadata +192 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Integrations
|
|
3
|
+
class Boxnow < Spree::Integration
|
|
4
|
+
preference :client_id, :string
|
|
5
|
+
preference :client_secret, :string
|
|
6
|
+
preference :partner_id, :string
|
|
7
|
+
preference :origin_location_id, :string
|
|
8
|
+
preference :api_url, :string
|
|
9
|
+
preference :contact_name, :string
|
|
10
|
+
preference :contact_phone, :string
|
|
11
|
+
preference :contact_email, :string
|
|
12
|
+
|
|
13
|
+
validates :preferred_client_id, presence: true
|
|
14
|
+
validates :preferred_client_secret, presence: true
|
|
15
|
+
validates :preferred_partner_id, presence: true
|
|
16
|
+
validates :preferred_origin_location_id, presence: true
|
|
17
|
+
validates :preferred_api_url, presence: true
|
|
18
|
+
validates :preferred_contact_phone, presence: true
|
|
19
|
+
validates :preferred_contact_email, presence: true
|
|
20
|
+
|
|
21
|
+
def self.integration_group
|
|
22
|
+
'shipping'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.icon_path
|
|
26
|
+
'integration_icons/boxnow-logo.png'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module OrderDecorator
|
|
3
|
+
def can_create_boxnow_voucher?
|
|
4
|
+
shipments.any?(&:can_create_boxnow_voucher?)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def can_print_boxnow_voucher?
|
|
8
|
+
shipments.any?(&:can_print_boxnow_voucher?)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def can_cancel_boxnow_voucher?
|
|
12
|
+
shipments.any?(&:can_cancel_boxnow_voucher?)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Spree::Order.prepend Spree::OrderDecorator
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module ShipmentDecorator
|
|
3
|
+
def can_create_boxnow_voucher?
|
|
4
|
+
!tracked? && ready? && shipping_method.boxnow?
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def can_print_boxnow_voucher?
|
|
8
|
+
tracked? && shipping_method.boxnow?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def can_cancel_boxnow_voucher?
|
|
12
|
+
tracked? && shipping_method&.boxnow? && !shipped?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Spree::Shipment.prepend Spree::ShipmentDecorator
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module SpreeBoxnow
|
|
2
|
+
class CancelVoucher
|
|
3
|
+
attr_reader :shipment
|
|
4
|
+
|
|
5
|
+
def initialize(shipment)
|
|
6
|
+
@shipment = shipment
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
parcel_id = shipment.tracking
|
|
11
|
+
raise VoucherError, 'No BoxNow parcel ID on this shipment' if parcel_id.blank?
|
|
12
|
+
|
|
13
|
+
SpreeBoxnow::ApiClient.new.cancel_parcel(parcel_id)
|
|
14
|
+
|
|
15
|
+
shipment.tracking = nil
|
|
16
|
+
shipment.private_metadata.delete('boxnow.vg_child')
|
|
17
|
+
shipment.save!
|
|
18
|
+
rescue SpreeBoxnow::ApiClient::ApiError => e
|
|
19
|
+
raise VoucherError, e.message
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class VoucherError < StandardError; end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module SpreeBoxnow
|
|
2
|
+
class CreateVoucher
|
|
3
|
+
include Spree::IntegrationsConcern
|
|
4
|
+
|
|
5
|
+
attr_reader :shipment, :order
|
|
6
|
+
|
|
7
|
+
def initialize(shipment)
|
|
8
|
+
@shipment = shipment
|
|
9
|
+
@order = shipment.order
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
integration = store_integration('boxnow')
|
|
14
|
+
raise VoucherError, 'BoxNow integration not configured' unless integration
|
|
15
|
+
|
|
16
|
+
destination_id = shipment.private_metadata['boxnow.destination_location_id']
|
|
17
|
+
raise VoucherError, 'No BoxNow locker selected for this shipment' if destination_id.blank?
|
|
18
|
+
|
|
19
|
+
address = shipment.address || order.ship_address
|
|
20
|
+
cod = order.payment_method&.cod_payment?
|
|
21
|
+
|
|
22
|
+
attempt = (shipment.private_metadata['boxnow.voucher_attempt'].to_i + 1)
|
|
23
|
+
shipment.private_metadata['boxnow.voucher_attempt'] = attempt
|
|
24
|
+
order_number = attempt == 1 ? shipment.number : "#{shipment.number}-#{attempt}"
|
|
25
|
+
|
|
26
|
+
params = {
|
|
27
|
+
orderNumber: order_number,
|
|
28
|
+
invoiceValue: order.total.to_s,
|
|
29
|
+
paymentMode: cod ? 'cod' : 'prepaid',
|
|
30
|
+
amountToBeCollected: cod ? shipment.final_price_with_items.to_f.to_s : '0.00',
|
|
31
|
+
origin: {
|
|
32
|
+
locationId: integration.preferred_origin_location_id,
|
|
33
|
+
contactName: integration.preferred_contact_name.presence || '',
|
|
34
|
+
contactNumber: integration.preferred_contact_phone.presence || '',
|
|
35
|
+
contactEmail: integration.preferred_contact_email.presence || ''
|
|
36
|
+
},
|
|
37
|
+
destination: {
|
|
38
|
+
locationId: destination_id,
|
|
39
|
+
contactName: address.full_name,
|
|
40
|
+
contactNumber: address.phone.to_s,
|
|
41
|
+
contactEmail: order.email.to_s
|
|
42
|
+
},
|
|
43
|
+
items: [{
|
|
44
|
+
id: '1',
|
|
45
|
+
name: 'voucher',
|
|
46
|
+
value: '0.00',
|
|
47
|
+
compartmentSize: parcel_size,
|
|
48
|
+
weight: shipment.item_weight.to_f.round
|
|
49
|
+
}]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
result = SpreeBoxnow::ApiClient.new.create_delivery_request(params)
|
|
53
|
+
|
|
54
|
+
parcels = result['parcels'] || []
|
|
55
|
+
raise VoucherError, 'BoxNow returned no parcels' if parcels.empty?
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
parcel_id: parcels[0]['id'],
|
|
59
|
+
child_parcel_ids: parcels[1..].map { |p| p['id'] }
|
|
60
|
+
}
|
|
61
|
+
rescue SpreeBoxnow::ApiClient::ApiError => e
|
|
62
|
+
raise VoucherError, e.message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Returns BoxNow compartment size: 1=small, 2=medium, 3=large
|
|
68
|
+
# Mirrors the height thresholds in BoxnowRate calculator
|
|
69
|
+
def parcel_size
|
|
70
|
+
height = shipment.manifest.sum { |item| item.variant.height.to_f * item.quantity }
|
|
71
|
+
if height <= 8.0 then 1
|
|
72
|
+
elsif height <= 17.0 then 2
|
|
73
|
+
else 3
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
class VoucherError < StandardError; end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SpreeBoxnow
|
|
2
|
+
class PrintVouchers
|
|
3
|
+
attr_reader :parcel_id
|
|
4
|
+
|
|
5
|
+
def initialize(parcel_id)
|
|
6
|
+
@parcel_id = parcel_id
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
SpreeBoxnow::ApiClient.new.fetch_label(parcel_id, format: 'pdf')
|
|
11
|
+
rescue SpreeBoxnow::ApiClient::ApiError => e
|
|
12
|
+
raise VoucherError, e.message
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class VoucherError < StandardError; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="col-12 col-md-6 p-0">
|
|
2
|
+
<div class="card mb-4">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<%= preference_field(@integration, form, 'client_id', i18n_scope: 'admin.integrations.boxnow') %>
|
|
5
|
+
<%= preference_field(@integration, form, 'client_secret', i18n_scope: 'admin.integrations.boxnow') %>
|
|
6
|
+
<%= preference_field(@integration, form, 'partner_id', i18n_scope: 'admin.integrations.boxnow') %>
|
|
7
|
+
<%= preference_field(@integration, form, 'origin_location_id', i18n_scope: 'admin.integrations.boxnow') %>
|
|
8
|
+
<%= preference_field(@integration, form, 'api_url', i18n_scope: 'admin.integrations.boxnow') %>
|
|
9
|
+
<%= preference_field(@integration, form, 'contact_name', i18n_scope: 'admin.integrations.boxnow') %>
|
|
10
|
+
<%= preference_field(@integration, form, 'contact_phone', i18n_scope: 'admin.integrations.boxnow') %>
|
|
11
|
+
<%= preference_field(@integration, form, 'contact_email', i18n_scope: 'admin.integrations.boxnow') %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<% order ||= shipment.order %>
|
|
2
|
+
<%= turbo_frame_tag dom_id(shipment), class: "card rounded-lg" do %>
|
|
3
|
+
<div class="card-header items-center justify-between">
|
|
4
|
+
<div class="flex items-center gap-2">
|
|
5
|
+
<span class="flex items-center"><%= shipment_state(shipment.state) %></span>
|
|
6
|
+
<strong class="shipment-number"><%= shipment.number %></strong>
|
|
7
|
+
<% if shipment.shipped_at.present? %>
|
|
8
|
+
<small class="text-gray-600">
|
|
9
|
+
<%= spree_time_ago(shipment.shipped_at) %>
|
|
10
|
+
</small>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<span class="flex justify-between items-center gap-2">
|
|
15
|
+
<span class="text-gray-600"><%= Spree.t(:package_from).downcase %></span>
|
|
16
|
+
<span class="font-semibold">
|
|
17
|
+
<% if can?(:edit, shipment.stock_location) %>
|
|
18
|
+
<%= link_to shipment.stock_location.name, spree.edit_admin_stock_location_path(shipment.stock_location), data: { turbo_frame: '_top' } %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= shipment.stock_location.name %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</strong>
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="card-body mb-0 p-0">
|
|
26
|
+
<div class="shipment-line-items">
|
|
27
|
+
<div class="text-gray-600 text-center border-b py-2 px-4 text-sm shipment-line-items__row">
|
|
28
|
+
<span class="text-left">
|
|
29
|
+
<%= Spree.t(:item_description) %>
|
|
30
|
+
</span>
|
|
31
|
+
<span>
|
|
32
|
+
<%= Spree.t(:price) %>
|
|
33
|
+
</span>
|
|
34
|
+
<span>
|
|
35
|
+
<%= Spree.t(:quantity) %>
|
|
36
|
+
</span>
|
|
37
|
+
<span>
|
|
38
|
+
<%= Spree.t(:total) %>
|
|
39
|
+
</span>
|
|
40
|
+
<span>
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
<%= render collection: shipment.manifest, partial: 'spree/admin/orders/shipment_manifest_item', as: :item, locals: { shipment: shipment } %>
|
|
44
|
+
</div>
|
|
45
|
+
<%= turbo_frame_tag dom_id(shipment, :shipping_method), class: "py-2 px-4 flex justify-between items-center min-h-14" do %>
|
|
46
|
+
<div>
|
|
47
|
+
<% if rate = shipment.selected_shipping_rate %>
|
|
48
|
+
<strong><%= rate.name %></strong>
|
|
49
|
+
<span>
|
|
50
|
+
<%= shipment.display_cost %>
|
|
51
|
+
</span>
|
|
52
|
+
<% else %>
|
|
53
|
+
<%= Spree.t(:no_shipping_method_selected) %>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<% if( (can? :update, shipment) and !shipment.shipped? && !order.canceled? && !shipment.canceled?) %>
|
|
58
|
+
<%= link_to_with_icon 'edit', Spree.t(:edit), spree.edit_admin_order_shipment_path(order, shipment), class: 'btn btn-sm btn-light', no_text: true %>
|
|
59
|
+
<% end %>
|
|
60
|
+
<% end %>
|
|
61
|
+
|
|
62
|
+
<% unless shipment.digital? %>
|
|
63
|
+
<%= turbo_frame_tag dom_id(shipment, :tracking), class: "border-t px-4 flex justify-between items-center min-h-14" do %>
|
|
64
|
+
<div class="py-4">
|
|
65
|
+
<strong><%= Spree.t(:tracking) %>: </strong>
|
|
66
|
+
<% if shipment.tracked? %>
|
|
67
|
+
<% if shipment.tracking_url.present? %>
|
|
68
|
+
<span><%= shipment_tracking_link_to(shipment: shipment, html_options: { target: '_blank', rel: 'noopener noreferrer' }) %></span>
|
|
69
|
+
<% elsif shipment.tracking.present? %>
|
|
70
|
+
<span><%= shipment.tracking %></span>
|
|
71
|
+
<% end %>
|
|
72
|
+
<% else %>
|
|
73
|
+
<span class="text-gray-600"><%= Spree.t(:no_tracking_present) %></span>
|
|
74
|
+
<% end %>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<% if can?(:update, shipment) && (!order.canceled? && !shipment.canceled?) %>
|
|
78
|
+
<% if shipment.tracking.present? %>
|
|
79
|
+
<%= link_to_with_icon 'edit', Spree.t(:edit), spree.edit_admin_order_shipment_path(order, shipment), class: 'btn btn-light btn-sm', no_text: true, title: Spree.t(:edit) %>
|
|
80
|
+
<% else %> <%= link_to_with_icon 'plus', Spree.t(:add_one), spree.edit_admin_order_shipment_path(order, shipment), class: 'btn btn-light btn-sm', no_text: true, title: Spree.t(:add_one) %>
|
|
81
|
+
<% end %>
|
|
82
|
+
<% end %>
|
|
83
|
+
<% end %>
|
|
84
|
+
<% end %>
|
|
85
|
+
|
|
86
|
+
<% if shipment.shipping_method&.boxnow? %>
|
|
87
|
+
<%
|
|
88
|
+
integration = Spree::Store.current&.integrations&.active&.find { |i| i.type == 'Spree::Integrations::Boxnow' }
|
|
89
|
+
locker_name = shipment.private_metadata['boxnow.locker_name']
|
|
90
|
+
locker_address = shipment.private_metadata['boxnow.locker_address']
|
|
91
|
+
can_edit_locker = integration.present? && (can? :update, shipment) && !order.canceled? && !shipment.canceled? && !shipment.shipped?
|
|
92
|
+
picker_id = "admin-boxnow-picker-#{shipment.id}"
|
|
93
|
+
%>
|
|
94
|
+
<div class="border-t px-4 py-3" id="admin-boxnow-row-<%= shipment.id %>">
|
|
95
|
+
<div class="flex items-center justify-between">
|
|
96
|
+
<div>
|
|
97
|
+
<strong>BoxNow locker: </strong>
|
|
98
|
+
<span id="admin-boxnow-locker-name-<%= shipment.id %>"><%= locker_name %></span>
|
|
99
|
+
<% unless locker_name.present? %>
|
|
100
|
+
<span class="text-gray-500" id="admin-boxnow-locker-placeholder-<%= shipment.id %>"><%= Spree.t('boxnow.no_locker_selected') %></span>
|
|
101
|
+
<% end %>
|
|
102
|
+
<span class="text-gray-500 text-sm ml-1" id="admin-boxnow-locker-address-<%= shipment.id %>"><%= locker_address %></span>
|
|
103
|
+
</div>
|
|
104
|
+
<% if can_edit_locker %>
|
|
105
|
+
<button type="button" class="btn btn-sm btn-light"
|
|
106
|
+
onclick="window['adminBoxnowOpen_<%= shipment.id %>']()">
|
|
107
|
+
<%= locker_name.present? ? Spree.t('boxnow.change_locker') : Spree.t('boxnow.select_locker') %>
|
|
108
|
+
</button>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<% if can_edit_locker %>
|
|
113
|
+
<div id="<%= picker_id %>" class="hidden mt-3">
|
|
114
|
+
<div id="admin-boxnow-map-<%= shipment.id %>" style="height:600px; width:100%"></div>
|
|
115
|
+
<a href="javascript:;" class="boxnow-map-widget-button" style="display:none"></a>
|
|
116
|
+
</div>
|
|
117
|
+
<script>
|
|
118
|
+
(function() {
|
|
119
|
+
var shId = '<%= shipment.id %>';
|
|
120
|
+
var pickerId = '<%= picker_id %>';
|
|
121
|
+
var partnerId = <%= raw integration.preferred_partner_id.to_json %>;
|
|
122
|
+
var endpoint = '<%= spree.admin_boxnow_select_locker_path(order.id) %>';
|
|
123
|
+
|
|
124
|
+
function csrfToken() {
|
|
125
|
+
var m = document.querySelector('meta[name="csrf-token"]');
|
|
126
|
+
return m ? m.content : '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function openWidget() {
|
|
130
|
+
window._bn_map_widget_config = {
|
|
131
|
+
partnerId: partnerId,
|
|
132
|
+
parentElement: '#admin-boxnow-map-' + shId,
|
|
133
|
+
type: 'iframe',
|
|
134
|
+
afterSelect: function(selected) {
|
|
135
|
+
var lockerId = selected.boxnowLockerId;
|
|
136
|
+
var lockerName = selected.boxnowLockerName || selected.boxnowLockerAddressLine1;
|
|
137
|
+
var lockerAddress = [selected.boxnowLockerAddressLine1, selected.boxnowLockerPostalCode].filter(Boolean).join(', ');
|
|
138
|
+
|
|
139
|
+
fetch(endpoint, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken() },
|
|
142
|
+
body: JSON.stringify({ locker_id: lockerId, locker_name: lockerName, locker_address: lockerAddress })
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
document.getElementById('admin-boxnow-locker-name-' + shId).textContent = lockerName;
|
|
146
|
+
document.getElementById('admin-boxnow-locker-address-' + shId).textContent = lockerAddress;
|
|
147
|
+
var placeholder = document.getElementById('admin-boxnow-locker-placeholder-' + shId);
|
|
148
|
+
if (placeholder) placeholder.style.display = 'none';
|
|
149
|
+
document.getElementById(pickerId).classList.add('hidden');
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (document.getElementById('boxnow-widget-script')) {
|
|
154
|
+
var btn = document.querySelector('.boxnow-map-widget-button');
|
|
155
|
+
if (btn) setTimeout(function() { btn.click(); }, 50);
|
|
156
|
+
} else {
|
|
157
|
+
var s = document.createElement('script');
|
|
158
|
+
s.id = 'boxnow-widget-script';
|
|
159
|
+
s.src = 'https://widget-cdn.boxnow.gr/map-widget/client/v5.js';
|
|
160
|
+
s.async = true;
|
|
161
|
+
s.onload = function() {
|
|
162
|
+
var btn = document.querySelector('.boxnow-map-widget-button');
|
|
163
|
+
if (btn) setTimeout(function() { btn.click(); }, 50);
|
|
164
|
+
};
|
|
165
|
+
document.head.appendChild(s);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
window['adminBoxnowOpen_' + shId] = function() {
|
|
170
|
+
var picker = document.getElementById(pickerId);
|
|
171
|
+
picker.classList.toggle('hidden');
|
|
172
|
+
if (!picker.classList.contains('hidden')) openWidget();
|
|
173
|
+
};
|
|
174
|
+
})();
|
|
175
|
+
</script>
|
|
176
|
+
<% end %>
|
|
177
|
+
</div>
|
|
178
|
+
<% end %>
|
|
179
|
+
|
|
180
|
+
<% if can_ship?(shipment) %>
|
|
181
|
+
<div class="card-footer flex justify-end border-t bg-gray-25">
|
|
182
|
+
<% if shipment.tracked? %>
|
|
183
|
+
<%= link_to_with_icon 'send.svg', Spree.t(:ship), spree.ship_admin_order_shipment_path(order, shipment), class: 'ml-auto btn btn-primary mb-0', data: {turbo_method: :post, turbo_confirm: Spree.t(:are_you_sure)} %>
|
|
184
|
+
<% elsif !shipment.digital? %>
|
|
185
|
+
<span class="ship ml-auto btn btn-primary disabled mb-0" data-controller="tooltip" data-tooltip-placement-value="left">
|
|
186
|
+
<%= icon 'send' %>
|
|
187
|
+
<%= Spree.t(:ship) %>
|
|
188
|
+
<%= tooltip(Spree.t('admin.shipment.please_provide_tracking_details')) %>
|
|
189
|
+
</span>
|
|
190
|
+
<% else %>
|
|
191
|
+
<span class="text-gray-600 w-full">
|
|
192
|
+
<%= Spree.t('admin.digital_shipment_fulfillment_note') %>
|
|
193
|
+
</span>
|
|
194
|
+
<% end %>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
</div>
|
|
198
|
+
<% end %>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div class="card mb-4">
|
|
2
|
+
<div class="card-header">
|
|
3
|
+
<h5 class="card-title">
|
|
4
|
+
<%= Spree.t('admin.integrations.boxnow.title') %>
|
|
5
|
+
</h5>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
<div class="form-check">
|
|
9
|
+
<%= f.check_box :boxnow, class: 'form-check-input' %>
|
|
10
|
+
<%= f.label :boxnow, class: 'form-check-label' %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%
|
|
2
|
+
boxnow_integration = Spree::Store.current&.integrations&.active&.find { |i| i.type == 'Spree::Integrations::Boxnow' }
|
|
3
|
+
is_boxnow = rate.shipping_method.boxnow?
|
|
4
|
+
is_selected = ship_form.object.selected_shipping_rate_id.to_s == rate.id.to_s
|
|
5
|
+
%>
|
|
6
|
+
<li class="list-group-item p-0 m-0 border-b delivery-list-item border-default" data-checkout-delivery-target="shippingRate" id="<%= spree_dom_id(rate) %>">
|
|
7
|
+
<div class="custom-control custom-radio flex items-center px-5 py-4">
|
|
8
|
+
<%= ship_form.radio_button :selected_shipping_rate_id,
|
|
9
|
+
rate.id,
|
|
10
|
+
class: 'custom-control-input radio-input',
|
|
11
|
+
id: "shipping-rate-#{rate.id}",
|
|
12
|
+
data: {
|
|
13
|
+
action: 'click->checkout-delivery#update',
|
|
14
|
+
cost: rate.display_cost,
|
|
15
|
+
tax: rate.display_tax_amount
|
|
16
|
+
} %>
|
|
17
|
+
<label class="select-none cursor-pointer px-2 word-break w-full" for="<%= "shipping-rate-#{rate.id}" %>">
|
|
18
|
+
<%= rate.name %>
|
|
19
|
+
<% if rate.delivery_range %>
|
|
20
|
+
<span class="text-muted text-xs ml-3"><%= rate.display_delivery_range %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
</label>
|
|
23
|
+
<span class="text-right">
|
|
24
|
+
<%= rate.cost <= 0 ? Spree.t(:free) : rate.display_cost %>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<% if is_boxnow && boxnow_integration.present? %>
|
|
29
|
+
<%= render 'spree_boxnow/locker_picker', integration: boxnow_integration, shipment: ship_form.object, rate: rate, selected: is_selected %>
|
|
30
|
+
<% end %>
|
|
31
|
+
</li>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= javascript_import_module_tag 'application-spree-boxnow' %>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<%
|
|
2
|
+
saved_locker_id = shipment.private_metadata['boxnow.destination_location_id']
|
|
3
|
+
saved_locker_name = shipment.private_metadata['boxnow.locker_name']
|
|
4
|
+
saved_locker_address = shipment.private_metadata['boxnow.locker_address']
|
|
5
|
+
%>
|
|
6
|
+
<div class="px-5 pb-5 pt-2 border-t border-default boxnow-locker-picker" id="boxnow-locker-picker-<%= rate.id %>" style="<%= selected ? '' : 'display:none' %>">
|
|
7
|
+
<div id="boxnow-map-container" style="height:600px; width:100%"></div>
|
|
8
|
+
|
|
9
|
+
<% if saved_locker_id.present? %>
|
|
10
|
+
<div id="boxnow-selected-locker" class="mb-3 p-3 bg-green-50 rounded-md border border-green-200">
|
|
11
|
+
<p class="text-sm font-medium text-green-800">
|
|
12
|
+
<%= Spree.t('boxnow.locker_selected') %>:
|
|
13
|
+
<span id="boxnow-locker-name" class="font-bold"><%= saved_locker_name %></span>
|
|
14
|
+
</p>
|
|
15
|
+
<p class="text-xs text-green-700" id="boxnow-locker-address"><%= saved_locker_address %></p>
|
|
16
|
+
</div>
|
|
17
|
+
<% else %>
|
|
18
|
+
<div id="boxnow-selected-locker" class="hidden mb-3 p-3 bg-green-50 rounded-md border border-green-200">
|
|
19
|
+
<p class="text-sm font-medium text-green-800">
|
|
20
|
+
<%= Spree.t('boxnow.locker_selected') %>:
|
|
21
|
+
<span id="boxnow-locker-name" class="font-bold"></span>
|
|
22
|
+
</p>
|
|
23
|
+
<p class="text-xs text-green-700" id="boxnow-locker-address"></p>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<a href="javascript:;" class="boxnow-map-widget-button" style="display:none"></a>
|
|
28
|
+
|
|
29
|
+
<script>
|
|
30
|
+
window._bn_map_widget_config = {
|
|
31
|
+
partnerId: <%= raw integration.preferred_partner_id.to_json %>,
|
|
32
|
+
parentElement: "#boxnow-map-container",
|
|
33
|
+
type: "iframe",
|
|
34
|
+
afterSelect: function(selected) {
|
|
35
|
+
var lockerId = selected.boxnowLockerId;
|
|
36
|
+
var lockerName = selected.boxnowLockerName || selected.boxnowLockerAddressLine1;
|
|
37
|
+
var lockerAddress = [selected.boxnowLockerAddressLine1, selected.boxnowLockerPostalCode].filter(Boolean).join(', ');
|
|
38
|
+
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
|
39
|
+
|
|
40
|
+
fetch("<%= spree.boxnow_select_locker_path %>", {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
"X-CSRF-Token": csrfMeta ? csrfMeta.content : ''
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ locker_id: lockerId, locker_name: lockerName, locker_address: lockerAddress })
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
document.getElementById("boxnow-locker-name").textContent = lockerName;
|
|
50
|
+
document.getElementById("boxnow-locker-address").textContent = lockerAddress;
|
|
51
|
+
document.getElementById("boxnow-selected-locker").classList.remove("hidden");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
(function(d) {
|
|
56
|
+
var pickerId = 'boxnow-locker-picker-<%= rate.id %>';
|
|
57
|
+
var radioId = 'shipping-rate-<%= rate.id %>';
|
|
58
|
+
|
|
59
|
+
function openWidget() {
|
|
60
|
+
var btn = d.querySelector('.boxnow-map-widget-button');
|
|
61
|
+
if (btn) btn.click();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadWidget(callback) {
|
|
65
|
+
if (d.getElementById('boxnow-widget-script')) {
|
|
66
|
+
callback();
|
|
67
|
+
} else {
|
|
68
|
+
var e = d.createElement("script");
|
|
69
|
+
e.id = "boxnow-widget-script";
|
|
70
|
+
e.src = "https://widget-cdn.boxnow.gr/map-widget/client/v5.js";
|
|
71
|
+
e.async = true;
|
|
72
|
+
e.defer = true;
|
|
73
|
+
e.onload = callback;
|
|
74
|
+
d.getElementsByTagName("head")[0].appendChild(e);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showPickerAndOpen() {
|
|
79
|
+
var picker = d.getElementById(pickerId);
|
|
80
|
+
if (picker) picker.style.display = '';
|
|
81
|
+
d.querySelectorAll('.boxnow-locker-picker').forEach(function(el) {
|
|
82
|
+
if (el.id !== pickerId) el.style.display = 'none';
|
|
83
|
+
});
|
|
84
|
+
loadWidget(function() { setTimeout(openWidget, 50); });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var radio = d.getElementById(radioId);
|
|
88
|
+
if (radio) {
|
|
89
|
+
radio.addEventListener('change', function() {
|
|
90
|
+
if (radio.checked) showPickerAndOpen();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Auto-open if already selected on page load
|
|
95
|
+
var picker = d.getElementById(pickerId);
|
|
96
|
+
if (picker && picker.style.display !== 'none') {
|
|
97
|
+
loadWidget(function() { setTimeout(openWidget, 50); });
|
|
98
|
+
}
|
|
99
|
+
})(document);
|
|
100
|
+
</script>
|
|
101
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<% if store_integration('boxnow').present? %>
|
|
2
|
+
<% if @order.can_create_boxnow_voucher? %>
|
|
3
|
+
<button
|
|
4
|
+
type="button"
|
|
5
|
+
class='btn text-left dropdown-item'
|
|
6
|
+
data-controller='spree-boxnow'
|
|
7
|
+
data-action='spree-boxnow#createVoucher'
|
|
8
|
+
data-spree-boxnow-order-id-value='<%= @order.id %>'
|
|
9
|
+
data-spree-boxnow-create-voucher-prompt-value="<%= Spree.t('admin.integrations.boxnow.create_voucher_prompt') %>"
|
|
10
|
+
data-spree-boxnow-create-voucher-error-value="<%= Spree.t('admin.integrations.boxnow.create_voucher_error') %>">
|
|
11
|
+
<%= icon 'package' %>
|
|
12
|
+
<%= Spree.t('admin.integrations.boxnow.create_voucher') %>
|
|
13
|
+
</button>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% if @order.can_cancel_boxnow_voucher? %>
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class='btn text-left dropdown-item'
|
|
20
|
+
data-controller='spree-boxnow'
|
|
21
|
+
data-action='spree-boxnow#cancelVoucher'
|
|
22
|
+
data-spree-boxnow-order-id-value='<%= @order.id %>'
|
|
23
|
+
data-spree-boxnow-cancel-voucher-prompt-value="<%= Spree.t('admin.integrations.boxnow.cancel_voucher_prompt') %>"
|
|
24
|
+
data-spree-boxnow-cancel-voucher-error-value="<%= Spree.t('admin.integrations.boxnow.cancel_voucher_error') %>">
|
|
25
|
+
<%= icon 'x' %>
|
|
26
|
+
<%= Spree.t('admin.integrations.boxnow.cancel_voucher') %>
|
|
27
|
+
</button>
|
|
28
|
+
<% end %>
|
|
29
|
+
|
|
30
|
+
<% if @order.can_print_boxnow_voucher? %>
|
|
31
|
+
<%= link_to_with_icon 'printer',
|
|
32
|
+
Spree.t('admin.integrations.boxnow.print_voucher'),
|
|
33
|
+
spree.admin_boxnow_print_path(@order.id),
|
|
34
|
+
class: 'btn text-left dropdown-item',
|
|
35
|
+
target: '_blank'
|
|
36
|
+
%>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pin 'application-spree-boxnow', to: 'spree_boxnow/application.js', preload: false
|
|
2
|
+
|
|
3
|
+
pin_all_from SpreeBoxnow::Engine.root.join('app/javascript/spree_boxnow/controllers'),
|
|
4
|
+
under: 'spree_boxnow/controllers',
|
|
5
|
+
to: 'spree_boxnow/controllers',
|
|
6
|
+
preload: 'application-spree-boxnow'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Rails.application.config.after_initialize do
|
|
2
|
+
Spree.integrations << Spree::Integrations::Boxnow
|
|
3
|
+
|
|
4
|
+
Rails.application.config.spree.calculators.shipping_methods << Spree::Calculator::Shipping::BoxnowRate
|
|
5
|
+
|
|
6
|
+
# Admin partials
|
|
7
|
+
Spree.admin.partials.head << 'spree_boxnow/head'
|
|
8
|
+
Spree.admin.partials.order_page_dropdown << 'spree_boxnow/order_dropdown_options'
|
|
9
|
+
Spree.admin.partials.shipping_method_form << 'spree/admin/shipping_methods/boxnow_form'
|
|
10
|
+
|
|
11
|
+
Spree::PermittedAttributes.shipping_method_attributes << :boxnow
|
|
12
|
+
end
|