spree_delhivery 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +175 -0
  3. data/Rakefile +21 -0
  4. data/app/assets/config/spree_delhivery_manifest.js +4 -0
  5. data/app/assets/images/integration_icons/delhivery.png +0 -0
  6. data/app/assets/images/payment_icons/delhivery.svg +12 -0
  7. data/app/assets/images/payment_icons/delhivery_cod.svg +12 -0
  8. data/app/controllers/spree/admin/delhivery_controller.rb +190 -0
  9. data/app/controllers/spree/admin/delhivery_returns_controller.rb +82 -0
  10. data/app/controllers/spree/admin/fulfillments_controller.rb +117 -0
  11. data/app/controllers/spree/admin/shipments_controller_decorator.rb +198 -0
  12. data/app/controllers/spree/admin/stock_locations_controller_decorator.rb +38 -0
  13. data/app/controllers/spree/api/v3/store/delhivery_controller.rb +126 -0
  14. data/app/jobs/spree_delhivery/base_job.rb +5 -0
  15. data/app/models/spree/calculator/shipping/delhivery.rb +97 -0
  16. data/app/models/spree/integrations/delhivery.rb +48 -0
  17. data/app/models/spree/order_decorator.rb +63 -0
  18. data/app/models/spree/page_blocks/products/delhivery_edd.rb +42 -0
  19. data/app/models/spree/page_sections/product_details_decorator.rb +26 -0
  20. data/app/models/spree/payment_method/delhivery_cod.rb +57 -0
  21. data/app/services/spree_delhivery/client.rb +281 -0
  22. data/app/services/spree_delhivery/pickup_service.rb +49 -0
  23. data/app/services/spree_delhivery/shipment_canceler.rb +59 -0
  24. data/app/services/spree_delhivery/shipment_sender.rb +210 -0
  25. data/app/services/spree_delhivery/shipment_tracker.rb +50 -0
  26. data/app/views/spree/admin/fulfillments/new.html.erb +118 -0
  27. data/app/views/spree/admin/integrations/forms/_delhivery.html.erb +51 -0
  28. data/app/views/spree/admin/orders/_shipment.html.erb +180 -0
  29. data/app/views/spree/admin/orders/return_authorizations/_return_authorization.html.erb +157 -0
  30. data/app/views/spree/admin/page_blocks/forms/_delhivery_edd.html.erb +157 -0
  31. data/app/views/spree/admin/payment_methods/configuration_guides/_delhivery_cod.html.erb +71 -0
  32. data/app/views/spree/admin/payment_methods/descriptions/_delhivery_cod.html.erb +7 -0
  33. data/app/views/spree/admin/return_authorizations/index.html.erb +143 -0
  34. data/app/views/spree/admin/shipments/edit.html.erb +40 -0
  35. data/app/views/spree/admin/stock_locations/_delhivery_fields.html.erb +19 -0
  36. data/app/views/spree/admin/stock_locations/_form.html.erb +184 -0
  37. data/app/views/spree/checkout/payment/_delhivery_cod.html.erb +9 -0
  38. data/app/views/spree/page_blocks/products/delhivery_edd/_delhivery_edd.html.erb +239 -0
  39. data/app/views/spree_delhivery/_head.html.erb +0 -0
  40. data/config/importmap.rb +6 -0
  41. data/config/initializers/spree.rb +15 -0
  42. data/config/initializers/spree_permitted_attributes.rb +4 -0
  43. data/config/locales/en.yml +36 -0
  44. data/config/routes.rb +42 -0
  45. data/db/migrate/20250101000001_add_delhivery_fields_to_shipments.rb +10 -0
  46. data/db/migrate/20250101000002_add_tracking_status_to_shipments.rb +13 -0
  47. data/db/migrate/20251227110851_add_delhivery_fields_to_spree_stock_locations.rb +5 -0
  48. data/db/migrate/20251227112401_add_geolocation_to_stock_locations.rb +9 -0
  49. data/db/migrate/20251227123158_add_missing_coordinates_to_stock_locations.rb +18 -0
  50. data/db/migrate/20251228081459_add_delhivery_to_return_authorizations.rb +8 -0
  51. data/lib/generators/spree_delhivery/install/install_generator.rb +139 -0
  52. data/lib/spree_delhivery/configuration.rb +13 -0
  53. data/lib/spree_delhivery/engine.rb +39 -0
  54. data/lib/spree_delhivery/factories.rb +6 -0
  55. data/lib/spree_delhivery/version.rb +7 -0
  56. data/lib/spree_delhivery.rb +13 -0
  57. data/lib/tasks/delhivery.rake +60 -0
  58. metadata +151 -0
@@ -0,0 +1,157 @@
1
+ <%# Admin form for Delhivery EDD block %>
2
+ <br><h5 class="mb-3">Content Settings</h5><br>
3
+
4
+ <div class="form-group">
5
+ <%= f.label :preferred_heading_text, "Heading Label" %>
6
+ <%= f.text_field :preferred_heading_text, class: "form-control", data: { action: "input->auto-submit#submit" } %>
7
+ </div>
8
+
9
+ <div class="row">
10
+ <div class="col-6">
11
+ <div class="form-group">
12
+ <%= f.label :preferred_placeholder_text, "Placeholder Text" %>
13
+ <%= f.text_field :preferred_placeholder_text, class: "form-control", data: { action: "input->auto-submit#submit" } %>
14
+ </div>
15
+ </div>
16
+ <div class="col-6">
17
+ <div class="form-group">
18
+ <%= f.label :preferred_button_text, "Button Text" %>
19
+ <%= f.text_field :preferred_button_text, class: "form-control", data: { action: "input->auto-submit#submit" } %>
20
+ </div>
21
+ </div>
22
+ </div>
23
+
24
+ <hr>
25
+ <br><h5 class="mb-3">Logic Settings (Order Timer)</h5><br>
26
+
27
+ <%# Row 1: Time Settings %>
28
+ <div class="row mb-3">
29
+ <div class="col-6">
30
+ <div class="form-group">
31
+ <%= f.label :preferred_cutoff_hour, "Cutoff Hour (1-12)" %>
32
+ <%= f.number_field :preferred_cutoff_hour, class: "form-control", min: 1, max: 12, data: { action: "input->auto-submit#submit" } %>
33
+ </div>
34
+ </div>
35
+ <div class="col-6">
36
+ <div class="form-group">
37
+ <%= f.label :preferred_cutoff_meridiem, "AM / PM" %>
38
+ <%= f.select :preferred_cutoff_meridiem, ['AM', 'PM'], {}, class: "form-control select2", data: { action: "change->auto-submit#submit" } %>
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <%# Row 2: Shipping Mode (Full Width) %>
44
+ <div class="row">
45
+ <div class="col-12">
46
+ <div class="form-group">
47
+ <%= f.label :preferred_default_mode, "Shipping Mode" %>
48
+ <%= f.select :preferred_default_mode, [['Surface Only', 'Surface'], ['Express Only', 'Express'], ['Both (Allow Switching)', 'Both']], {}, class: "form-control select2", data: { action: "change->auto-submit#submit" } %>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <hr>
54
+
55
+ <br><h5 class="mb-3">Appearance (Colors)</h5><br>
56
+
57
+ <div class="mb-4 form-group"
58
+ data-controller="color-picker"
59
+ data-color-picker-clear-value="false"
60
+ data-color-picker-default-color-value="<%= f.object.preferred_button_bg_color.presence || '#000000' %>">
61
+ <div class="d-flex align-items-center">
62
+ <%= f.hidden_field :preferred_button_bg_color,
63
+ data: { color_picker_target: "input", action: "change->auto-submit#submit" },
64
+ autocomplete: "off" %>
65
+
66
+ <div>
67
+ <label class="mb-0">Button Background</label><br>
68
+ <span data-color-picker-target="value" class="text-muted">
69
+ <%= f.object.preferred_button_bg_color.presence || '#000000' %>
70
+ </span>
71
+ </div>
72
+
73
+ <div class="ml-auto" style="width:40px">
74
+ <div data-color-picker-target="picker"
75
+ class="border d-inline-block rounded-circle"
76
+ style="height: 40px; width: 40px; background-color: <%= f.object.preferred_button_bg_color.presence || '#000000' %>;"
77
+ role="button" aria-label="toggle color picker dialog"></div>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="mb-4 form-group"
83
+ data-controller="color-picker"
84
+ data-color-picker-clear-value="false"
85
+ data-color-picker-default-color-value="<%= f.object.preferred_button_text_color.presence || '#FFFFFF' %>">
86
+ <div class="d-flex align-items-center">
87
+ <%= f.hidden_field :preferred_button_text_color,
88
+ data: { color_picker_target: "input", action: "change->auto-submit#submit" },
89
+ autocomplete: "off" %>
90
+
91
+ <div>
92
+ <label class="mb-0">Button Text Color</label><br>
93
+ <span data-color-picker-target="value" class="text-muted">
94
+ <%= f.object.preferred_button_text_color.presence || '#FFFFFF' %>
95
+ </span>
96
+ </div>
97
+
98
+ <div class="ml-auto" style="width:40px">
99
+ <div data-color-picker-target="picker"
100
+ class="border d-inline-block rounded-circle"
101
+ style="height: 40px; width: 40px; background-color: <%= f.object.preferred_button_text_color.presence || '#FFFFFF' %>;"
102
+ role="button"></div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <hr>
108
+
109
+ <div class="mb-4 form-group"
110
+ data-controller="color-picker"
111
+ data-color-picker-clear-value="false"
112
+ data-color-picker-default-color-value="<%= f.object.preferred_success_color.presence || '#10B981' %>">
113
+ <div class="d-flex align-items-center">
114
+ <%= f.hidden_field :preferred_success_color,
115
+ data: { color_picker_target: "input", action: "change->auto-submit#submit" },
116
+ autocomplete: "off" %>
117
+
118
+ <div>
119
+ <label class="mb-0">Success Message Color</label><br>
120
+ <span data-color-picker-target="value" class="text-muted">
121
+ <%= f.object.preferred_success_color.presence || '#10B981' %>
122
+ </span>
123
+ </div>
124
+
125
+ <div class="ml-auto" style="width:40px">
126
+ <div data-color-picker-target="picker"
127
+ class="border d-inline-block rounded-circle"
128
+ style="height: 40px; width: 40px; background-color: <%= f.object.preferred_success_color.presence || '#10B981' %>;"
129
+ role="button"></div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="mb-4 form-group"
135
+ data-controller="color-picker"
136
+ data-color-picker-clear-value="false"
137
+ data-color-picker-default-color-value="<%= f.object.preferred_error_color.presence || '#EF4444' %>">
138
+ <div class="d-flex align-items-center">
139
+ <%= f.hidden_field :preferred_error_color,
140
+ data: { color_picker_target: "input", action: "change->auto-submit#submit" },
141
+ autocomplete: "off" %>
142
+
143
+ <div>
144
+ <label class="mb-0">Error Message Color</label><br>
145
+ <span data-color-picker-target="value" class="text-muted">
146
+ <%= f.object.preferred_error_color.presence || '#EF4444' %>
147
+ </span>
148
+ </div>
149
+
150
+ <div class="ml-auto" style="width:40px">
151
+ <div data-color-picker-target="picker"
152
+ class="border d-inline-block rounded-circle"
153
+ style="height: 40px; width: 40px; background-color: <%= f.object.preferred_error_color.presence || '#EF4444' %>;"
154
+ role="button"></div>
155
+ </div>
156
+ </div>
157
+ </div>
@@ -0,0 +1,71 @@
1
+ <%
2
+ base_url = current_store.url_or_custom_domain.to_s
3
+ base_url = "https://#{base_url}" unless base_url.start_with?("http")
4
+ base_url = base_url.chomp('/')
5
+ %>
6
+
7
+ <style>
8
+ .delhivery-premium-ui .badge-event {
9
+ font-weight: 500;
10
+ background-color: #f0fdf4;
11
+ border: 1px solid #bbf7d0;
12
+ color: #166534;
13
+ padding: 0.35em 0.75em;
14
+ border-radius: 0.375rem;
15
+ font-size: 0.8rem;
16
+ }
17
+ </style>
18
+
19
+ <div class="delhivery-premium-ui">
20
+ <div class="card mb-3 border shadow-sm" style="border-radius: 0.5rem;">
21
+ <div class="card-body d-flex align-items-center py-3 px-4">
22
+ <div class="mr-3 text-muted d-flex align-items-center">
23
+ <%= icon 'shield-check', width: 20, height: 20 %>
24
+ </div>
25
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
26
+ Configure your Delhivery Production Token and Client ID below. Find your authorization details in your
27
+ <strong><%= external_link_to 'Delhivery One Dashboard', 'https://one.delhivery.com/', target: '_blank', class: 'alert-link font-weight-bold text-primary' %></strong>.
28
+ </p>
29
+ </div>
30
+ </div>
31
+
32
+ <% if @payment_method.persisted? %>
33
+ <div class="card mb-4 border shadow-sm" style="border-radius: 0.5rem;">
34
+ <div class="card-body d-flex align-items-center py-3 px-4 border-bottom">
35
+ <div class="mr-3 text-muted d-flex align-items-center">
36
+ <%= icon 'refresh', width: 20, height: 20 %>
37
+ </div>
38
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
39
+ <strong>Automated Fulfillment Reconciliation:</strong> This payment method safely hooks into your tracking engine to trigger auto-captures.
40
+ </p>
41
+ </div>
42
+
43
+ <div class="card-body py-4 px-4">
44
+ <div class="mb-2">
45
+ <h6 class="font-weight-bold text-dark mb-2" style="font-size: 0.95rem;">Tracking Engine Hooks</h6>
46
+ <p class="text-muted small mb-3">
47
+ When tracking updates are dispatched or requested, the database scans for the following lifecycle flags to settle cash accounts automatically:
48
+ </p>
49
+
50
+ <div class="d-flex align-items-center flex-wrap">
51
+ <span class="text-dark font-weight-bold mr-3" style="font-size: 0.85rem;">Monitored Flags:</span>
52
+ <span class="badge badge-event mr-2 mb-1">DELIVERED</span>
53
+ <span class="badge badge-event mr-2 mb-1">CANCELLED</span>
54
+ <span class="badge badge-event mb-1">REJECTED / RTO</span>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <% else %>
60
+ <div class="card mb-4 border shadow-sm" style="border-radius: 0.5rem; border-left: 4px solid #0ea5e9 !important;">
61
+ <div class="card-body d-flex align-items-center py-3 px-4">
62
+ <div class="mr-3 text-info d-flex align-items-center">
63
+ <%= icon 'info-circle', width: 22, height: 22 %>
64
+ </div>
65
+ <p class="mb-0 text-dark" style="font-size: 0.95rem;">
66
+ <strong>Configuration Initialization Pending:</strong> Click <strong>Create</strong> to save this payment method instance before linking logistics status checks.
67
+ </p>
68
+ </div>
69
+ </div>
70
+ <% end %>
71
+ </div>
@@ -0,0 +1,7 @@
1
+ <p class="mb-1 text-muted">
2
+ Delhivery Cash on Delivery (COD) allows customers to settle their order balances with cash or local digital collection methods right at their doorstep.
3
+ </p>
4
+
5
+ <div class="d-flex align-items-center mt-2">
6
+ <small class="text-gray-500">Automated tracking status synchronization enabled</small>
7
+ </div>
@@ -0,0 +1,143 @@
1
+ <%= render partial: 'spree/admin/shared/order_tabs', locals: { current: :return_authorizations } %>
2
+
3
+ <% content_for :page_actions do %>
4
+ <% if @order.shipments.any?(&:shipped?) %>
5
+ <%= button_link_to Spree.t(:new_return_authorization), new_admin_order_return_authorization_url(@order), class: "btn-success", icon: 'add', id: 'admin_new_return_authorization' %>
6
+ <% end %>
7
+ <% end %>
8
+
9
+ <% if @return_authorizations.any? %>
10
+ <table class="table">
11
+ <thead>
12
+ <tr data-hook="return_authorizations_header">
13
+ <th><%= Spree.t(:number) %></th>
14
+ <th><%= Spree.t(:state) %></th>
15
+ <th><%= Spree.t(:amount) %></th>
16
+ <th><%= Spree.t(:created_at) %></th>
17
+ <th>Delhivery Return</th>
18
+ <th class="actions"></th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ <% @return_authorizations.each do |return_authorization| %>
23
+ <tr id="<%= dom_id(return_authorization) %>" data-hook="return_authorization_row">
24
+ <td><%= return_authorization.number %></td>
25
+ <td>
26
+ <span class="label label-<%= return_authorization.state %>"><%= Spree.t("return_authorization_states.#{return_authorization.state}") %></span>
27
+ </td>
28
+ <td><%= return_authorization.display_amount.to_html %></td>
29
+ <td><%= pretty_time(return_authorization.created_at) %></td>
30
+
31
+ <td>
32
+ <% if return_authorization.respond_to?(:delhivery_waybill) && return_authorization.delhivery_waybill.present? %>
33
+ <div class="badge badge-success" style="background-color: #28a745;">
34
+ <%= icon('check') %> <%= return_authorization.delhivery_waybill %>
35
+ </div>
36
+ <% else %>
37
+
38
+ <button type="button"
39
+ class="btn btn-sm btn-outline-primary"
40
+ data-toggle="modal"
41
+ data-target="#rvpModal-<%= return_authorization.id %>">
42
+ <%= icon('truck') %> Schedule Pickup
43
+ </button>
44
+
45
+ <div class="modal fade" id="rvpModal-<%= return_authorization.id %>" tabindex="-1" role="dialog" aria-hidden="true">
46
+ <div class="modal-dialog modal-dialog-centered" role="document">
47
+ <div class="modal-content">
48
+ <div class="modal-header">
49
+ <h5 class="modal-title font-weight-bold">
50
+ <%= icon('truck', class: "mr-1") %> Schedule Reverse Pickup
51
+ </h5>
52
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close">
53
+ <span aria-hidden="true">&times;</span>
54
+ </button>
55
+ </div>
56
+
57
+ <%= form_with url: spree.delhivery_create_pickup_admin_return_authorization_path(return_authorization), method: :post, local: true do |f| %>
58
+ <div class="modal-body text-left">
59
+
60
+ <div class="alert alert-info text-sm mb-3">
61
+ <strong>Return To:</strong> <%= return_authorization.stock_location.name %>
62
+ </div>
63
+
64
+ <h6 class="font-weight-bold mb-2 border-bottom pb-1">Parametric QC Details</h6>
65
+
66
+ <div class="row">
67
+ <div class="col-6">
68
+ <div class="form-group">
69
+ <label class="small font-weight-bold">Brand Name <span class="text-danger">*</span></label>
70
+ <%# Default to Store Name %>
71
+ <%= f.text_field :brand, value: current_store.name, class: "form-control", required: true %>
72
+ </div>
73
+ </div>
74
+ <div class="col-6">
75
+ <div class="form-group">
76
+ <label class="small font-weight-bold">Product Category <span class="text-danger">*</span></label>
77
+ <%# Default to 'General' %>
78
+ <%= f.text_field :category, value: "General", class: "form-control", required: true %>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="form-group">
84
+ <label class="small font-weight-bold">QC Question Set</label>
85
+ <select class="form-control" disabled title="Currently using Generic QC">
86
+ <option>Generic / Visual Check (Default)</option>
87
+ </select>
88
+ <small class="text-muted">Specific question sets require ID mapping configuration.</small>
89
+ </div>
90
+
91
+ <div class="bg-light p-2 rounded border mb-3">
92
+ <label class="small font-weight-bold">Items to Pickup:</label>
93
+ <% return_authorization.return_items.each do |item| %>
94
+ <div class="d-flex align-items-center mb-1 pl-2">
95
+ <%= icon('cube', class: "text-muted mr-2") %>
96
+ <span class="text-sm font-weight-bold"><%= item.inventory_unit.variant.name %></span>
97
+ <span class="badge badge-secondary ml-auto">Qty: 1</span>
98
+ </div>
99
+ <% end %>
100
+ </div>
101
+
102
+ <div class="form-check pt-2 border-top">
103
+ <input type="checkbox" class="form-check-input" id="confirmCheck-<%= return_authorization.id %>" required>
104
+ <label class="form-check-label text-sm" for="confirmCheck-<%= return_authorization.id %>">
105
+ I confirm the details and pickup location are correct.
106
+ </label>
107
+ </div>
108
+
109
+ </div>
110
+
111
+ <div class="modal-footer bg-light">
112
+ <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
113
+ <%= f.button class: "btn btn-primary" do %>
114
+ Confirm Pickup
115
+ <% end %>
116
+ </div>
117
+ <% end %>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <% end %>
122
+ </td>
123
+
124
+ <td class="actions actions-2 text-right">
125
+ <%= link_to_edit(return_authorization, no_text: true, class: 'edit') if can?(:edit, return_authorization) %>
126
+ <% if can?(:delete, return_authorization) %>
127
+ <% if return_authorization.can_cancel? %>
128
+ <%= link_to_with_icon 'delete', Spree.t(:cancel), spree.cancel_admin_order_return_authorization_path(@order, return_authorization), method: :put, data: { confirm: Spree.t(:are_you_sure) }, class: 'btn btn-danger btn-sm', no_text: true %>
129
+ <% else %>
130
+ <%= link_to_delete return_authorization, no_text: true %>
131
+ <% end %>
132
+ <% end %>
133
+ </td>
134
+ </tr>
135
+ <% end %>
136
+ </tbody>
137
+ </table>
138
+ <% else %>
139
+ <div class="alert alert-info no-objects-found">
140
+ <%= Spree.t(:no_resource_found, resource: Spree::ReturnAuthorization.model_name.human) %>,
141
+ <%= link_to Spree.t(:add_one), new_admin_order_return_authorization_url(@order) %>!
142
+ </div>
143
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <turbo-frame id="dialog">
2
+ <div class="dialog-content">
3
+ <div class="dialog-header">
4
+ <h5 class="dialog-title"><%= Spree.t(:edit_shipment) %> #<%= @shipment.number %></h5>
5
+ <button type="button" class="btn-close" data-action="dialog#close" aria-label="<%= Spree.t(:close) %>"></button>
6
+ </div>
7
+
8
+ <div class="dialog-body">
9
+ <%# We combine Tracking and Method into one form for the modal %>
10
+ <%= form_for [:admin, @order, @shipment], html: { id: 'edit-shipment-form' } do |f| %>
11
+
12
+ <%# Tracking Number Field %>
13
+ <div class="form-group mb-4">
14
+ <%= f.label :tracking, Spree.t(:tracking) %>
15
+ <%= f.text_field :tracking, class: 'form-control', autofocus: true %>
16
+ </div>
17
+
18
+ <%# Shipping Method Dropdown %>
19
+ <div class="form-group mb-4">
20
+ <%= f.label :selected_shipping_rate_id, Spree.t(:shipping_method) %>
21
+ <%= f.select :selected_shipping_rate_id,
22
+ @shipment.shipping_rates.sort_by(&:cost).map { |sr| ["#{sr.name} #{sr.display_price}", sr.id] },
23
+ {},
24
+ class: 'form-control custom-select' %>
25
+ </div>
26
+
27
+ <%# Hidden Field to ensure we stay on the same page context if needed %>
28
+ <%= hidden_field_tag :redirect_to_order, 'true' %>
29
+
30
+ <div class="dialog-footer d-flex justify-content-between align-items-center mt-4">
31
+ <button type="button" class="btn btn-secondary" data-action="dialog#close">
32
+ <%= Spree.t(:cancel) %>
33
+ </button>
34
+
35
+ <%= f.button Spree.t(:update), class: 'btn btn-primary', data: { disable_with: Spree.t(:saving) } %>
36
+ </div>
37
+ <% end %>
38
+ </div>
39
+ </div>
40
+ </turbo-frame>
@@ -0,0 +1,19 @@
1
+ <div class="card mb-3">
2
+ <div class="card-header">
3
+ <h5 class="card-title mb-0">Delhivery Settings</h5>
4
+ </div>
5
+ <div class="card-body">
6
+
7
+ <div class="form-group">
8
+ <%= label_tag :delhivery_warehouse_name, "Delhivery Warehouse Name (Exact Match)" %>
9
+ <%= text_field_tag "stock_location[delhivery_warehouse_name]",
10
+ @stock_location.delhivery_warehouse_name,
11
+ class: 'form-control',
12
+ placeholder: "e.g. Artolika.Inc" %>
13
+ <small class="form-text text-muted">
14
+ <strong>Critical:</strong> This must match the name registered in your Delhivery Client Panel exactly.
15
+ </small>
16
+ </div>
17
+
18
+ </div>
19
+ </div>
@@ -0,0 +1,184 @@
1
+ <%# 1. General Settings %>
2
+ <div class="card mb-4">
3
+ <div class="card-header">
4
+ <h5 class="card-title">
5
+ <%= Spree.t(:general_settings) %>
6
+ </h5>
7
+ </div>
8
+ <div class="card-body">
9
+ <%= f.spree_text_field :name, required: true, autofocus: f.object.new_record? %>
10
+ <%= f.spree_text_field :admin_name, label: Spree.t(:internal_name) %>
11
+ <% unless f.object.default? %>
12
+ <%= f.spree_check_box :active %>
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+
17
+ <%# 2. Geolocation (Production Map UI - FIXED) %>
18
+ <div class="card mb-4">
19
+ <div class="card-header">
20
+ <h5 class="card-title d-flex align-items-center">
21
+ <%= icon('map-pin', class: "mr-2") %>
22
+ <%= Spree.t(:geolocation, default: 'Warehouse Coordinates') %>
23
+ </h5>
24
+ </div>
25
+
26
+ <%# Removed p-0 to allow Spree card padding and fixed bottom radius %>
27
+ <div class="card-body position-relative">
28
+
29
+ <%# --- Search Bar Overlay --- %>
30
+ <div class="mb-3 d-flex gap-2 align-items-center">
31
+ <div class="input-group" style="max-width: 400px;">
32
+ <input type="text" id="map-search-input" class="form-control" placeholder="Search for a city, area, or street...">
33
+ <div class="input-group-append">
34
+ <button type="button" id="map-search-btn" class="btn btn-primary">
35
+ Search Location
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <small class="text-muted ml-2 d-none d-sm-inline">
40
+ <i class="ti ti-hand-finger"></i> Drag the blue marker to pinpoint exact location.
41
+ </small>
42
+ </div>
43
+
44
+ <%# --- Map Container --- %>
45
+ <%# Added z-index: 0 to ensure it doesnt overlap Spree dropdowns %>
46
+ <%# Added mb-3 for spacing from inputs below %>
47
+ <div id="stock-location-map" style="height: 400px; width: 100%; z-index: 0; background-color: #f0f0f0;" class="mb-3 rounded border"></div>
48
+
49
+ <%# --- Read-Only Coordinates Footer --- %>
50
+ <div class="row">
51
+ <div class="col-6">
52
+ <%# Use f.spree_text_field for Spree Default Styling %>
53
+ <%= f.spree_text_field :latitude, type: 'text', readonly: true, id: 'spree_latitude', label: "Latitude" %>
54
+ </div>
55
+ <div class="col-6">
56
+ <%# Use f.spree_text_field for Spree Default Styling %>
57
+ <%= f.spree_text_field :longitude, type: 'text', readonly: true, id: 'spree_longitude', label: "Longitude" %>
58
+ </div>
59
+ </div>
60
+
61
+ </div>
62
+ </div>
63
+
64
+ <%# 3. Address %>
65
+ <div class="card mb-4">
66
+ <div class="card-header">
67
+ <h5 class="card-title">
68
+ <%= Spree.t(:address) %>
69
+ </h5>
70
+ </div>
71
+ <div class="card-body pb-0">
72
+ <%= render 'spree/addresses/form',
73
+ address_name: 'address',
74
+ address_form: f,
75
+ address_type: 'stock_location',
76
+ address: f.object %>
77
+ </div>
78
+ </div>
79
+
80
+ <%= render "spree/admin/stock_locations/delhivery_fields", f: f %>
81
+
82
+ <%# --- MAP SCRIPTS (UNCHANGED) --- %>
83
+ <%# Load Leaflet CSS/JS from CDN %>
84
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
85
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
86
+
87
+ <script>
88
+ (function() {
89
+ // Variable to store map instance to prevent duplicates
90
+ var mapInstance = null;
91
+
92
+ function initStockLocationMap() {
93
+ var mapContainer = document.getElementById('stock-location-map');
94
+
95
+ // Safety Checks: Container must exist
96
+ if (!mapContainer) return;
97
+
98
+ // Check if map is already initialized on this container
99
+ if (mapContainer._leaflet_id) {
100
+ return;
101
+ }
102
+
103
+ // 1. Get Inputs
104
+ var latField = document.getElementById('spree_latitude');
105
+ var lngField = document.getElementById('spree_longitude');
106
+
107
+ // Default to Center of India if empty
108
+ var initialLat = parseFloat(latField.value) || 20.5937;
109
+ var initialLng = parseFloat(lngField.value) || 78.9629;
110
+ var initialZoom = (latField.value && lngField.value && latField.value != "0.0") ? 16 : 5;
111
+
112
+ // 2. Create Map
113
+ mapInstance = L.map('stock-location-map').setView([initialLat, initialLng], initialZoom);
114
+
115
+ // 3. Add Tiles
116
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
117
+ maxZoom: 19,
118
+ attribution: '© OpenStreetMap'
119
+ }).addTo(mapInstance);
120
+
121
+ // 4. Add Marker
122
+ var marker = L.marker([initialLat, initialLng], {draggable: true}).addTo(mapInstance);
123
+
124
+ // Function to update fields
125
+ function updateInputs(lat, lng) {
126
+ latField.value = lat.toFixed(6);
127
+ lngField.value = lng.toFixed(6);
128
+ }
129
+
130
+ // Event: Drag
131
+ marker.on('dragend', function(e) {
132
+ var position = marker.getLatLng();
133
+ updateInputs(position.lat, position.lng);
134
+ mapInstance.panTo(position);
135
+ });
136
+
137
+ // Event: Click
138
+ mapInstance.on('click', function(e) {
139
+ marker.setLatLng(e.latlng);
140
+ updateInputs(e.latlng.lat, e.latlng.lng);
141
+ });
142
+
143
+ // 5. Search Button Logic
144
+ var searchBtn = document.getElementById('map-search-btn');
145
+ if(searchBtn) {
146
+ searchBtn.addEventListener('click', function(e) {
147
+ e.preventDefault(); // Prevent form submit
148
+ var query = document.getElementById('map-search-input').value;
149
+ if(query.length > 2) {
150
+ fetch('https://nominatim.openstreetmap.org/search?format=json&q=' + query)
151
+ .then(response => response.json())
152
+ .then(data => {
153
+ if(data && data.length > 0) {
154
+ var lat = parseFloat(data[0].lat);
155
+ var lon = parseFloat(data[0].lon);
156
+
157
+ mapInstance.setView([lat, lon], 16);
158
+ marker.setLatLng([lat, lon]);
159
+ updateInputs(lat, lon);
160
+ } else {
161
+ alert("Location not found.");
162
+ }
163
+ })
164
+ .catch(err => console.error(err));
165
+ }
166
+ });
167
+ }
168
+
169
+ // *** VITAL FIX FOR WHITE MAP ***
170
+ // Turbo transitions sometimes render the map before the div has height.
171
+ // This forces a redraw 300ms after load.
172
+ setTimeout(function() {
173
+ mapInstance.invalidateSize();
174
+ }, 300);
175
+ }
176
+
177
+ // Hook into Turbo Load (Spree Standard)
178
+ document.addEventListener("turbo:load", initStockLocationMap);
179
+
180
+ // Fallback for standard load
181
+ document.addEventListener("DOMContentLoaded", initStockLocationMap);
182
+
183
+ })();
184
+ </script>
@@ -0,0 +1,9 @@
1
+ <div class="bg-blue-50 text-blue-800 p-4 rounded-md text-sm border border-blue-200 mb-4">
2
+ <div class="flex items-start">
3
+ <i class="ti ti-cash icon mt-0.5 mr-2 w-5 h-5 text-blue-600" style=""></i>
4
+ <div>
5
+ <strong class="block mb-1 text-base">Cash on Delivery (COD)</strong>
6
+ <span>Please keep exact change ready. The Delhivery courier will collect <strong><%= @order.display_total %></strong> at the time of delivery.</span>
7
+ </div>
8
+ </div>
9
+ </div>