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.
- checksums.yaml +7 -0
- data/README.md +175 -0
- data/Rakefile +21 -0
- data/app/assets/config/spree_delhivery_manifest.js +4 -0
- data/app/assets/images/integration_icons/delhivery.png +0 -0
- data/app/assets/images/payment_icons/delhivery.svg +12 -0
- data/app/assets/images/payment_icons/delhivery_cod.svg +12 -0
- data/app/controllers/spree/admin/delhivery_controller.rb +190 -0
- data/app/controllers/spree/admin/delhivery_returns_controller.rb +82 -0
- data/app/controllers/spree/admin/fulfillments_controller.rb +117 -0
- data/app/controllers/spree/admin/shipments_controller_decorator.rb +198 -0
- data/app/controllers/spree/admin/stock_locations_controller_decorator.rb +38 -0
- data/app/controllers/spree/api/v3/store/delhivery_controller.rb +126 -0
- data/app/jobs/spree_delhivery/base_job.rb +5 -0
- data/app/models/spree/calculator/shipping/delhivery.rb +97 -0
- data/app/models/spree/integrations/delhivery.rb +48 -0
- data/app/models/spree/order_decorator.rb +63 -0
- data/app/models/spree/page_blocks/products/delhivery_edd.rb +42 -0
- data/app/models/spree/page_sections/product_details_decorator.rb +26 -0
- data/app/models/spree/payment_method/delhivery_cod.rb +57 -0
- data/app/services/spree_delhivery/client.rb +281 -0
- data/app/services/spree_delhivery/pickup_service.rb +49 -0
- data/app/services/spree_delhivery/shipment_canceler.rb +59 -0
- data/app/services/spree_delhivery/shipment_sender.rb +210 -0
- data/app/services/spree_delhivery/shipment_tracker.rb +50 -0
- data/app/views/spree/admin/fulfillments/new.html.erb +118 -0
- data/app/views/spree/admin/integrations/forms/_delhivery.html.erb +51 -0
- data/app/views/spree/admin/orders/_shipment.html.erb +180 -0
- data/app/views/spree/admin/orders/return_authorizations/_return_authorization.html.erb +157 -0
- data/app/views/spree/admin/page_blocks/forms/_delhivery_edd.html.erb +157 -0
- data/app/views/spree/admin/payment_methods/configuration_guides/_delhivery_cod.html.erb +71 -0
- data/app/views/spree/admin/payment_methods/descriptions/_delhivery_cod.html.erb +7 -0
- data/app/views/spree/admin/return_authorizations/index.html.erb +143 -0
- data/app/views/spree/admin/shipments/edit.html.erb +40 -0
- data/app/views/spree/admin/stock_locations/_delhivery_fields.html.erb +19 -0
- data/app/views/spree/admin/stock_locations/_form.html.erb +184 -0
- data/app/views/spree/checkout/payment/_delhivery_cod.html.erb +9 -0
- data/app/views/spree/page_blocks/products/delhivery_edd/_delhivery_edd.html.erb +239 -0
- data/app/views/spree_delhivery/_head.html.erb +0 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/spree.rb +15 -0
- data/config/initializers/spree_permitted_attributes.rb +4 -0
- data/config/locales/en.yml +36 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20250101000001_add_delhivery_fields_to_shipments.rb +10 -0
- data/db/migrate/20250101000002_add_tracking_status_to_shipments.rb +13 -0
- data/db/migrate/20251227110851_add_delhivery_fields_to_spree_stock_locations.rb +5 -0
- data/db/migrate/20251227112401_add_geolocation_to_stock_locations.rb +9 -0
- data/db/migrate/20251227123158_add_missing_coordinates_to_stock_locations.rb +18 -0
- data/db/migrate/20251228081459_add_delhivery_to_return_authorizations.rb +8 -0
- data/lib/generators/spree_delhivery/install/install_generator.rb +139 -0
- data/lib/spree_delhivery/configuration.rb +13 -0
- data/lib/spree_delhivery/engine.rb +39 -0
- data/lib/spree_delhivery/factories.rb +6 -0
- data/lib/spree_delhivery/version.rb +7 -0
- data/lib/spree_delhivery.rb +13 -0
- data/lib/tasks/delhivery.rake +60 -0
- metadata +151 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module SpreeDelhivery
|
|
2
|
+
class ShipmentTracker
|
|
3
|
+
# Simple result object pattern
|
|
4
|
+
Result = Struct.new(:success?, :status, :data)
|
|
5
|
+
|
|
6
|
+
def initialize(shipment)
|
|
7
|
+
@shipment = shipment
|
|
8
|
+
@client = SpreeDelhivery::Client.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
unless @shipment.delhivery_waybill.present?
|
|
13
|
+
return Result.new(false, "No Waybill found for Shipment #{@shipment.number}")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
response = @client.track_shipment(@shipment.delhivery_waybill)
|
|
18
|
+
|
|
19
|
+
# Safe navigation to extract the Shipment object
|
|
20
|
+
# Delhivery structure: { "ShipmentData" => [ { "Shipment" => { ... } } ] }
|
|
21
|
+
shipment_data = response.dig('ShipmentData', 0, 'Shipment')
|
|
22
|
+
|
|
23
|
+
if shipment_data.present?
|
|
24
|
+
# 1. Extract Status safely
|
|
25
|
+
# "Status" object contains keys like "Status", "StatusDateTime", "RecievedBy"
|
|
26
|
+
current_status = shipment_data.dig('Status', 'Status') || "Unknown"
|
|
27
|
+
|
|
28
|
+
# 2. Update Database efficiently
|
|
29
|
+
# usage of update_columns avoids triggering callbacks/validations, which is preferred for background sync
|
|
30
|
+
@shipment.update_columns(
|
|
31
|
+
delhivery_response_data: shipment_data,
|
|
32
|
+
tracking_status: current_status
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
Rails.logger.info "[Delhivery] Synced Shipment #{@shipment.number}: #{current_status}"
|
|
36
|
+
|
|
37
|
+
return Result.new(true, current_status, shipment_data)
|
|
38
|
+
else
|
|
39
|
+
error_msg = "No Shipment Data found in Delhivery response"
|
|
40
|
+
Rails.logger.warn "[Delhivery] Tracking Failed for #{@shipment.number}: #{error_msg}"
|
|
41
|
+
return Result.new(false, error_msg)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Rails.logger.error "[Delhivery] Exception tracking #{@shipment.number}: #{e.message}"
|
|
46
|
+
return Result.new(false, e.message)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<turbo-frame id="dialog">
|
|
2
|
+
<div>
|
|
3
|
+
<div class="dialog-header">
|
|
4
|
+
<h5 class="dialog-title">Ship Package</h5>
|
|
5
|
+
<button name="button" type="button" class="btn-close" data-action="dialog#close" data-dismiss="dialog" aria-label="Close"></button>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%= form_with url: spree.admin_order_shipment_fulfillment_path(@order.number, @shipment.number), method: :post, data: { turbo_frame: "_top" } do |f| %>
|
|
9
|
+
<div class="dialog-body">
|
|
10
|
+
|
|
11
|
+
<div class="alert badge-info mb-4">
|
|
12
|
+
<div>
|
|
13
|
+
<h5 class="font-bold mb-1" style="font-size: 0.95rem;">
|
|
14
|
+
<i class="ti ti-info-circle mr-1"></i> Shipment Notice
|
|
15
|
+
</h5>
|
|
16
|
+
You are preparing to generate a manifest and ship package <strong><%= @shipment.number %></strong>.
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<%# LIVE PRICING SERVICE LEVEL DROPDOWN %>
|
|
21
|
+
<div class="form-group mb-4">
|
|
22
|
+
<label for="shipping_method_id" class="font-medium text-gray-900 mb-2 block">Shipping Service Level</label>
|
|
23
|
+
<select name="shipping_method_id" id="shipping_method_id" class="form-select w-full">
|
|
24
|
+
<% @available_methods.each do |method| %>
|
|
25
|
+
<option value="<%= method[:id] %>" <%= 'selected' if @shipment.selected_shipping_rate&.shipping_method_id == method[:id] %>>
|
|
26
|
+
<%= method[:name] %> (<%= method[:display_cost] %>)
|
|
27
|
+
</option>
|
|
28
|
+
<% end %>
|
|
29
|
+
</select>
|
|
30
|
+
<span class="form-text mt-2 text-xs text-gray-600">Changing this updates the customer's total to match the actual carrier rate.</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<%# LOGISTICS & DIMENSIONAL AUDIT BOX %>
|
|
34
|
+
<% if @integration.present? %>
|
|
35
|
+
<div class="mb-4 border border-gray-200 rounded-md bg-gray-50/70 p-4">
|
|
36
|
+
<div class="flex items-center gap-1.5 mb-3 border-b border-gray-200 pb-2">
|
|
37
|
+
<i class="ti ti-scale icon text-gray-700" style="font-size: 1.1rem;"></i>
|
|
38
|
+
<h6 class="text-sm font-semibold text-gray-900 m-0">Logistics & Dim Weight Verification</h6>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="grid grid-cols-1 gap-y-2 text-sm mb-4">
|
|
42
|
+
|
|
43
|
+
<div class="flex items-center justify-between">
|
|
44
|
+
<span class="text-muted">Catalog Store Weight:</span>
|
|
45
|
+
<span class="badge badge-balance_due border text-dark font-mono" style="font-size: 0.85rem;">
|
|
46
|
+
<%= @shipment.line_items.sum { |li| (li.variant.weight || 0) * li.quantity }.round(2) %> <%= @weight_unit %>
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="flex items-center justify-between">
|
|
51
|
+
<span class="text-muted">Delhivery Payload Weight:</span>
|
|
52
|
+
<span class="badge badge-balance_due border text-dark font-mono" style="font-size: 0.85rem;">
|
|
53
|
+
<%= @weight_in_grams %> g
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="flex items-center justify-between pt-1">
|
|
58
|
+
<span class="text-muted">Calculated Dimensions (L×W×H):</span>
|
|
59
|
+
<div class="flex items-center gap-2">
|
|
60
|
+
<% if @dims_cm.any? { |d| d == 10.0 } %>
|
|
61
|
+
<span class="badge badge-warning text-dark flex items-center" title="One or more items are using fallback defaults. Please review catalog metrics.">
|
|
62
|
+
<i class="ti ti-alert-triangle mr-1"></i> Fallback Used
|
|
63
|
+
</span>
|
|
64
|
+
<% end %>
|
|
65
|
+
<span class="badge badge-balance_due border text-dark font-mono" style="font-size: 0.85rem;">
|
|
66
|
+
<%= @dims_cm[0] %> × <%= @dims_cm[1] %> × <%= @dims_cm[2] %> CM
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<%# Dynamic Packaging Selection Rule %>
|
|
74
|
+
<div class="form-group mb-0 pt-3 border-t border-gray-200">
|
|
75
|
+
<label for="delhivery_packaging_type" class="font-medium text-gray-900 text-xs mb-1.5 block">Assigned Physical Packaging Profile</label>
|
|
76
|
+
<select name="delhivery_packaging_type" id="delhivery_packaging_type" class="form-select form-select-sm w-full font-sans">
|
|
77
|
+
<option value="Carton" <%= 'selected' if @recommended_packaging == 'Carton Box' %>>Carton Box (Large Profile / Fragile Frame Shipments)</option>
|
|
78
|
+
<option value="Flyer" <%= 'selected' if @recommended_packaging == 'Flyer' %>>Standard Document Flyer (Soft Materials < 2kg)</option>
|
|
79
|
+
</select>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
<% end %>
|
|
83
|
+
|
|
84
|
+
<label class="font-medium text-gray-900 mb-3 block">Fulfillment Action</label>
|
|
85
|
+
|
|
86
|
+
<%# 2. DELHIVERY PRIMARY ACTION %>
|
|
87
|
+
<% if defined?(Spree::Integrations::Delhivery) && Spree::Integrations::Delhivery.active.exists? %>
|
|
88
|
+
<div class="flex items-center gap-2 mb-3">
|
|
89
|
+
<input type="radio" name="fulfillment_type" value="delhivery" id="fulfillment_type_delhivery" checked class="form-radio text-primary focus:ring-primary" onchange="document.getElementById('manual_tracking_field').classList.add('hidden')">
|
|
90
|
+
<label for="fulfillment_type_delhivery" class="text-sm font-medium">Use Delhivery API (Auto-generates Waybill)</label>
|
|
91
|
+
</div>
|
|
92
|
+
<% end %>
|
|
93
|
+
|
|
94
|
+
<%# 3. MANUAL OVERRIDE ACTION %>
|
|
95
|
+
<div class="flex items-center gap-2 mb-3">
|
|
96
|
+
<input type="radio" name="fulfillment_type" value="manual" id="fulfillment_type_manual" class="form-radio text-primary focus:ring-primary" <%= 'checked' unless defined?(Spree::Integrations::Delhivery) %> onchange="document.getElementById('manual_tracking_field').classList.remove('hidden')">
|
|
97
|
+
<label for="fulfillment_type_manual" class="text-sm font-medium">Manual Tracking (Shipped outside API)</label>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<%# 4. CONDITIONAL TRACKING INPUT %>
|
|
101
|
+
<div id="manual_tracking_field" class="pl-6 pt-2 <%= 'hidden' if defined?(Spree::Integrations::Delhivery) %>">
|
|
102
|
+
<div class="form-group mb-0">
|
|
103
|
+
<label for="tracking_number">Tracking Number <span class="required font-weight-bold text-danger">*</span></label>
|
|
104
|
+
<%= f.text_field :tracking_number, class: "form-input w-full", placeholder: "e.g. BD77382910" %>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="dialog-footer">
|
|
111
|
+
<button name="button" type="button" class="btn btn-light" data-action="dialog#close" data-dismiss="dialog">Discard</button>
|
|
112
|
+
<button name="button" type="submit" class="btn btn-primary text-center flex items-center justify-center" data-turbo-submits-with="<span class='inline-block w-4 h-4 border-2 border-current border-r-transparent rounded-full animate-spin' role='status'></span>" style="width: 100px; height: 35px;">
|
|
113
|
+
Confirm
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
<% end %>
|
|
117
|
+
</div>
|
|
118
|
+
</turbo-frame>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="card mb-4">
|
|
2
|
+
<div class="card-header">
|
|
3
|
+
<h5 class="mb-0">Delhivery Settings</h5>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="card-body">
|
|
6
|
+
<%# API Token %>
|
|
7
|
+
<%= preference_field(@integration, form, 'api_token', i18n_scope: 'spree.admin.integrations.delhivery') %>
|
|
8
|
+
|
|
9
|
+
<%# Client Name (Optional) %>
|
|
10
|
+
<%= preference_field(@integration, form, 'client_name', i18n_scope: 'spree.admin.integrations.delhivery') %>
|
|
11
|
+
|
|
12
|
+
<%# Pickup Location - CRITICAL %>
|
|
13
|
+
<%= preference_field(@integration, form, 'pickup_location_name', i18n_scope: 'spree.admin.integrations.delhivery') %>
|
|
14
|
+
<small class="form-text text-muted mb-4">
|
|
15
|
+
Must match the exact Warehouse Name in your Delhivery Dashboard.
|
|
16
|
+
</small>
|
|
17
|
+
|
|
18
|
+
<%# --- NEW: COD SURCHARGE --- %>
|
|
19
|
+
<div class="form-group mb-4 bg-light p-3 border rounded">
|
|
20
|
+
<label class="font-weight-bold">COD Surcharge Fee (₹)</label>
|
|
21
|
+
<%= form.number_field :preferred_cod_surcharge_amount, class: 'form-control', step: '0.01' %>
|
|
22
|
+
<small class="form-text text-muted mt-2">
|
|
23
|
+
<strong>Optional:</strong> Enter a fixed amount to charge customers for using Cash on Delivery (e.g. 150). Set to 0 to disable.
|
|
24
|
+
</small>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="row mb-4">
|
|
28
|
+
<div class="col-6">
|
|
29
|
+
<div class="form-group">
|
|
30
|
+
<label>Store Weight Unit</label>
|
|
31
|
+
<%= form.select :preferred_store_weight_unit,
|
|
32
|
+
options_for_select([['Kilograms (kg)', 'kg'], ['Pounds (lbs)', 'lbs'], ['Grams (g)', 'g'], ['Ounces (oz)', 'oz']], @integration.preferred_store_weight_unit),
|
|
33
|
+
{}, class: 'form-control' %>
|
|
34
|
+
<small class="form-text text-muted mt-2">Select the unit your products use in Spree.</small>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="col-6">
|
|
38
|
+
<div class="form-group">
|
|
39
|
+
<label>Store Dimension Unit</label>
|
|
40
|
+
<%= form.select :preferred_store_dimension_unit,
|
|
41
|
+
options_for_select([['Centimeters (cm)', 'cm'], ['Inches (in)', 'in'], ['Meters (m)', 'm'], ['Millimeters (mm)', 'mm']], @integration.preferred_store_dimension_unit),
|
|
42
|
+
{}, class: 'form-control' %>
|
|
43
|
+
<small class="form-text text-muted mt-2">Select the unit your products use in Spree.</small>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<%# Production Mode Checkbox %>
|
|
49
|
+
<%= preference_field(@integration, form, 'production_mode', i18n_scope: 'spree.admin.integrations.delhivery') %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<% order ||= shipment.order %>
|
|
2
|
+
<%= turbo_frame_tag dom_id(shipment), class: "card mb-6 block w-full shadow-sm" do %>
|
|
3
|
+
|
|
4
|
+
<%# --- HEADER & TOOLBAR --- %>
|
|
5
|
+
<div class="card-header flex flex-col sm:flex-row sm:items-center justify-between px-4 py-4 gap-4 h-auto">
|
|
6
|
+
|
|
7
|
+
<div class="flex items-center gap-2 w-full sm:w-auto">
|
|
8
|
+
<span class="flex items-center"><%= shipment_state(shipment.state) %></span>
|
|
9
|
+
<strong class="shipment-number text-lg text-gray-900"><%= shipment.number %></strong>
|
|
10
|
+
<% if shipment.shipped_at.present? %>
|
|
11
|
+
<small class="text-gray-600 whitespace-nowrap ml-auto sm:ml-1">
|
|
12
|
+
<%= spree_time_ago(shipment.shipped_at) %>
|
|
13
|
+
</small>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="flex items-center gap-3 w-full sm:w-auto sm:ml-auto justify-end pt-2">
|
|
18
|
+
<div class="flex items-center gap-2">
|
|
19
|
+
|
|
20
|
+
<%# 1. DELHIVERY ACTIONS %>
|
|
21
|
+
<% if defined?(Spree::Integrations::Delhivery) && shipment.try(:delhivery_waybill).present? %>
|
|
22
|
+
|
|
23
|
+
<%= link_to spree.delhivery_label_admin_shipment_path(shipment, format: :pdf), class: 'btn btn-light btn-sm p-0 flex items-center justify-center border hover:bg-white', style: 'width: 28px; height: 28px;', target: '_blank', title: "Print Shipping Label" do %>
|
|
24
|
+
<i class="ti ti-printer icon" style=""></i>
|
|
25
|
+
<% end %>
|
|
26
|
+
<button type="button" class="btn btn-light btn-sm p-0 flex items-center justify-center border hover:bg-white" style="width: 28px; height: 28px;" onclick="document.getElementById('pickupModal-<%= shipment.id %>').showModal()" title="Schedule Pickup">
|
|
27
|
+
<i class="ti ti-truck icon" style=""></i>
|
|
28
|
+
</button>
|
|
29
|
+
<%= button_to spree.delhivery_track_admin_shipment_path(shipment), method: :post, class: 'btn btn-light btn-sm p-0 flex items-center justify-center border hover:bg-white', style: 'width: 28px; height: 28px;', title: "Refresh Status", data: { turbo: false } do %>
|
|
30
|
+
<i class="ti ti-refresh icon" style=""></i>
|
|
31
|
+
<% end %>
|
|
32
|
+
<%= button_to spree.delhivery_cancel_admin_shipment_path(shipment), method: :post, class: 'btn btn-light btn-sm p-0 flex items-center justify-center border hover:bg-white', style: 'width: 28px; height: 28px;', data: { turbo: false, confirm: "Void this Delhivery Waybill? This allows you to generate a new one." }, title: "Cancel Shipment" do %>
|
|
33
|
+
<i class="ti ti-x icon text-red-500" style=""></i>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<%# 2. OUR FULFILLMENT MODAL (Native Spree 5 Styling) %>
|
|
37
|
+
<% elsif !shipment.canceled? %>
|
|
38
|
+
|
|
39
|
+
<%= link_to spree.new_admin_order_shipment_fulfillment_path(order.number, shipment.number),
|
|
40
|
+
class: 'btn btn-secondary btn-sm',
|
|
41
|
+
data: { turbo_frame: 'dialog', action: 'dialog#open click->reveal:stop' } do %>
|
|
42
|
+
<i class="ti ti-send icon icon-send" style=""></i>
|
|
43
|
+
<span><%= shipment.shipped? ? 'Update Carrier' : Spree.t(:ship) %></span>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<%# LOCATION INFO %>
|
|
50
|
+
<div class="flex items-center gap-1 text-sm pl-3 border-l border-gray-200 ml-auto hidden sm:flex">
|
|
51
|
+
<small class="text-gray-600"><%= Spree.t(:package_from).downcase %></small>
|
|
52
|
+
<strong class="text-gray-900 truncate" style="max-width: 150px;">
|
|
53
|
+
<% if can?(:edit, shipment.stock_location) %>
|
|
54
|
+
<%= link_to shipment.stock_location.name, spree.edit_admin_stock_location_path(shipment.stock_location), data: { turbo_frame: '_top' } %>
|
|
55
|
+
<% else %>
|
|
56
|
+
<%= shipment.stock_location.name %>
|
|
57
|
+
<% end %>
|
|
58
|
+
</strong>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<%# --- BODY (Line Items) --- %>
|
|
64
|
+
<div class="card-body mb-0 p-0">
|
|
65
|
+
<div class="shipment-line-items overflow-x-auto">
|
|
66
|
+
<div class="text-gray-600 text-center border-b py-2 px-4 text-sm shipment-line-items__row bg-gray-50 min-w-[500px]">
|
|
67
|
+
<span class="text-left"><%= Spree.t(:item_description) %></span>
|
|
68
|
+
<span><%= Spree.t(:price) %></span>
|
|
69
|
+
<span><%= Spree.t(:quantity) %></span>
|
|
70
|
+
<span><%= Spree.t(:total) %></span>
|
|
71
|
+
<span></span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="min-w-[500px]">
|
|
74
|
+
<%= render collection: shipment.manifest, partial: 'spree/admin/orders/shipment_manifest_item', as: :item, locals: { shipment: shipment } %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<%= turbo_frame_tag dom_id(shipment, :shipping_method), class: "py-2 px-4 flex justify-between items-center min-h-14 border-t" do %>
|
|
79
|
+
<div class="flex items-center gap-2">
|
|
80
|
+
<% if rate = shipment.selected_shipping_rate %>
|
|
81
|
+
<strong><%= rate.name %></strong>
|
|
82
|
+
<span class="text-gray-600">(<%= shipment.display_cost %>)</span>
|
|
83
|
+
<% else %>
|
|
84
|
+
<%= Spree.t(:no_shipping_method_selected) %>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
|
|
89
|
+
<%# --- TRACKING --- %>
|
|
90
|
+
<% unless shipment.digital? %>
|
|
91
|
+
<%= turbo_frame_tag dom_id(shipment, :tracking), class: "border-t px-4 flex justify-between items-center min-h-14 bg-gray-50/50" do %>
|
|
92
|
+
<div class="py-4 flex items-center gap-2 mr-2">
|
|
93
|
+
<strong class="text-sm text-gray-900"><%= Spree.t(:tracking) %>:</strong>
|
|
94
|
+
<% if shipment.tracked? %>
|
|
95
|
+
<% if shipment.tracking_url.present? %>
|
|
96
|
+
<%= link_to shipment.tracking, shipment.tracking_url, target: '_blank', class: "text-blue-600 hover:underline font-mono text-sm font-medium" %>
|
|
97
|
+
<% else %>
|
|
98
|
+
<span class="font-mono text-gray-800 text-sm font-medium"><%= shipment.tracking %></span>
|
|
99
|
+
<% end %>
|
|
100
|
+
<% else %>
|
|
101
|
+
<span class="text-gray-400 italic text-sm"><%= Spree.t(:no_tracking_present) %></span>
|
|
102
|
+
<% end %>
|
|
103
|
+
|
|
104
|
+
<%# UNIFIED EDIT BUTTON (Native Spree 5 Styling) %>
|
|
105
|
+
<% if can?(:update, shipment) && !order.canceled? && !shipment.canceled? && shipment.try(:delhivery_waybill).blank? %>
|
|
106
|
+
<%= link_to spree.new_admin_order_shipment_fulfillment_path(order.number, shipment.number),
|
|
107
|
+
class: 'btn btn-light btn-sm ml-2 p-0 flex items-center justify-center border hover:bg-white',
|
|
108
|
+
style: 'width: 28px; height: 28px;',
|
|
109
|
+
title: "Update Carrier & Tracking",
|
|
110
|
+
data: { turbo_frame: 'dialog', action: 'dialog#open' } do %>
|
|
111
|
+
<i class="ti ti-pencil icon mr-0" style=""></i>
|
|
112
|
+
<% end %>
|
|
113
|
+
<% end %>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<%# Delhivery Status Badge %>
|
|
117
|
+
<% if shipment.try(:tracking_status).present? %>
|
|
118
|
+
<%
|
|
119
|
+
status_down = shipment.tracking_status.to_s.downcase
|
|
120
|
+
badge_class = case status_down
|
|
121
|
+
when 'delivered' then 'bg-green-100 text-green-800'
|
|
122
|
+
when 'shipped', 'in transit', 'out for delivery' then 'bg-blue-100 text-blue-800'
|
|
123
|
+
when 'canceled', 'rto' then 'bg-red-100 text-red-800'
|
|
124
|
+
else 'bg-gray-100 text-gray-800'
|
|
125
|
+
end
|
|
126
|
+
%>
|
|
127
|
+
<small class="px-2.5 py-0.5 rounded-full text-xs font-medium <%= badge_class %> flex items-center gap-1">
|
|
128
|
+
<i class="ti ti-truck icon" style="width: 14px; height: 14px;"></i>
|
|
129
|
+
<%= shipment.tracking_status.upcase %>
|
|
130
|
+
</small>
|
|
131
|
+
<% end %>
|
|
132
|
+
<% end %>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
<% end %>
|
|
136
|
+
|
|
137
|
+
<%# --- HTML5 PICKUP MODAL --- %>
|
|
138
|
+
<% if defined?(Spree::Integrations::Delhivery) && shipment.try(:delhivery_waybill).present? %>
|
|
139
|
+
<dialog id="pickupModal-<%= shipment.id %>" class="dialog">
|
|
140
|
+
<div>
|
|
141
|
+
<div class="dialog-header">
|
|
142
|
+
<h5 class="dialog-title">Schedule Pickup</h5>
|
|
143
|
+
<button type="button" class="btn-close" onclick="document.getElementById('pickupModal-<%= shipment.id %>').close()" aria-label="Close"></button>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<%= form_with url: spree.delhivery_pickup_admin_stock_location_path(shipment.stock_location), method: :post, data: { turbo: false } do |f| %>
|
|
147
|
+
<div class="dialog-body">
|
|
148
|
+
<div class="alert alert-info text-sm mb-4 bg-blue-50 text-blue-800 p-3 rounded-md flex items-center">
|
|
149
|
+
<i class="ti ti-info-circle icon mr-2 w-5 h-5" style=""></i>
|
|
150
|
+
<span>Requesting pickup for: <strong><%= shipment.stock_location.name %></strong></span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="flex gap-4">
|
|
154
|
+
<div class="form-group w-1/2">
|
|
155
|
+
<label for="pickup_date">Pickup Date</label>
|
|
156
|
+
<%= f.date_field :pickup_date, value: Date.today, min: Date.today, class: "form-input", id: "pickup_date" %>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="form-group w-1/2">
|
|
159
|
+
<label for="pickup_time">Pickup Time</label>
|
|
160
|
+
<%= f.time_field :pickup_time, value: "16:00", class: "form-input", id: "pickup_time" %>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="form-group mt-4">
|
|
165
|
+
<label for="count">Total Packages to Pickup <span class="required font-weight-bold text-danger"> *</span></label>
|
|
166
|
+
<%= f.number_field :count, value: 1, min: 1, class: "form-input", id: "count" %>
|
|
167
|
+
<span class="form-text mt-2">Total packages ready at this location today.</span>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<div class="dialog-footer">
|
|
172
|
+
<button type="button" class="btn btn-light" onclick="document.getElementById('pickupModal-<%= shipment.id %>').close()">Discard</button>
|
|
173
|
+
<button type="submit" class="btn btn-primary text-center flex items-center justify-center" data-turbo-submits-with="<span class='inline-block w-4 h-4 border-2 border-current border-r-transparent rounded-full animate-spin' role='status'></span>">
|
|
174
|
+
<i class="ti ti-truck icon mr-2" style=""></i> Schedule
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
<% end %>
|
|
178
|
+
</div>
|
|
179
|
+
</dialog>
|
|
180
|
+
<% end %>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<% edit_link =
|
|
2
|
+
if can?(:edit, return_authorization)
|
|
3
|
+
spree.edit_admin_order_return_authorization_path(
|
|
4
|
+
@order,
|
|
5
|
+
return_authorization,
|
|
6
|
+
)
|
|
7
|
+
else
|
|
8
|
+
spree.admin_order_return_authorization_path(@order, return_authorization)
|
|
9
|
+
end %>
|
|
10
|
+
|
|
11
|
+
<tr
|
|
12
|
+
id="<%= spree_dom_id(return_authorization) %>"
|
|
13
|
+
data-controller="row-link"
|
|
14
|
+
class="cursor-pointer"
|
|
15
|
+
>
|
|
16
|
+
<td data-action="click->row-link#openLink" class="font-bolder">
|
|
17
|
+
<%= link_to return_authorization.number, edit_link %>
|
|
18
|
+
</td>
|
|
19
|
+
<td data-action="click->row-link#openLink">
|
|
20
|
+
<small class="badge badge-<%= return_authorization.state %>">
|
|
21
|
+
<%= Spree.t("return_authorization_states.#{return_authorization.state}") %>
|
|
22
|
+
</small>
|
|
23
|
+
</td>
|
|
24
|
+
<td data-action="click->row-link#openLink"><%= return_authorization.display_pre_tax_total.to_html %></td>
|
|
25
|
+
<td data-action="click->row-link#openLink"><%= spree_time(return_authorization.created_at) %></td>
|
|
26
|
+
|
|
27
|
+
<%# --- DELHIVERY RETURN ACTION --- %>
|
|
28
|
+
<td class="text-right">
|
|
29
|
+
<div class="flex items-center justify-end gap-2">
|
|
30
|
+
|
|
31
|
+
<%# Only show if Delhivery integration exists %>
|
|
32
|
+
<% if defined?(Spree::Integrations::Delhivery) && Spree::Integrations::Delhivery.active.exists? %>
|
|
33
|
+
|
|
34
|
+
<%# 1. SUCCESS STATE: Waybill Exists %>
|
|
35
|
+
<% if return_authorization.respond_to?(:delhivery_waybill) && return_authorization.delhivery_waybill.present? %>
|
|
36
|
+
<div class="badge badge-success text-white" title="Waybill: <%= return_authorization.delhivery_waybill %>" style="background-color: #28a745;">
|
|
37
|
+
<%= icon('check', class: "w-3 h-3 mr-1") %>
|
|
38
|
+
Pickup Scheduled
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<% else %>
|
|
42
|
+
|
|
43
|
+
<%# 2. TRIGGER BUTTON (Opens Modal) %>
|
|
44
|
+
<%# Note: onclick with stopPropagation prevents the row-click event %>
|
|
45
|
+
<button type="button"
|
|
46
|
+
class="btn btn-sm btn-outline-primary shadow-sm stop-propagation"
|
|
47
|
+
onclick="event.stopPropagation(); $('#rvpModal-<%= return_authorization.id %>').modal('show');"
|
|
48
|
+
title="Schedule Reverse Pickup via Delhivery">
|
|
49
|
+
<%= icon('truck', class: "w-4 h-4 mr-1") %> Pickup
|
|
50
|
+
</button>
|
|
51
|
+
|
|
52
|
+
<%# 3. PICKUP FORM MODAL %>
|
|
53
|
+
<div class="modal fade text-left" id="rvpModal-<%= return_authorization.id %>" tabindex="-1" role="dialog" aria-hidden="true" onclick="event.stopPropagation();">
|
|
54
|
+
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
55
|
+
<div class="modal-content">
|
|
56
|
+
<div class="modal-header">
|
|
57
|
+
<h5 class="modal-title font-weight-bold">
|
|
58
|
+
<%= icon('truck', class: "mr-1") %> Schedule Reverse Pickup
|
|
59
|
+
</h5>
|
|
60
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
61
|
+
<span aria-hidden="true">×</span>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<%# FORM START %>
|
|
66
|
+
<%= form_with url: spree.delhivery_create_pickup_admin_return_authorization_path(return_authorization), method: :post, local: true do |f| %>
|
|
67
|
+
<div class="modal-body">
|
|
68
|
+
|
|
69
|
+
<div class="alert alert-info text-sm mb-3 w-100">
|
|
70
|
+
<strong>Return To:</strong> <%= return_authorization.stock_location.name %>
|
|
71
|
+
<br>
|
|
72
|
+
<span class="text-muted text-xs">
|
|
73
|
+
(Ensure "Delhivery Warehouse Name"
|
|
74
|
+
<br>is configured for this location)
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<h6 class="font-weight-bold mb-2 border-bottom pb-1">Parametric QC Details</h6>
|
|
79
|
+
|
|
80
|
+
<div class="row">
|
|
81
|
+
<div class="col-6">
|
|
82
|
+
<div class="form-group">
|
|
83
|
+
<label class="small font-weight-bold">Brand Name <span class="text-danger">*</span></label>
|
|
84
|
+
<%# Pre-fill with Store Name %>
|
|
85
|
+
<%= f.text_field :brand, value: current_store.name, class: "form-control", required: true %>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div class="col-6">
|
|
89
|
+
<div class="form-group">
|
|
90
|
+
<label class="small font-weight-bold">Category <span class="text-danger">*</span></label>
|
|
91
|
+
<%# Default to General %>
|
|
92
|
+
<%= f.text_field :category, value: "General", class: "form-control", required: true %>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="form-group">
|
|
98
|
+
<label class="small font-weight-bold">QC Question Set</label>
|
|
99
|
+
<select class="form-control" disabled title="Using Generic Visual Check">
|
|
100
|
+
<option>Generic / Visual Check (Default)</option>
|
|
101
|
+
</select>
|
|
102
|
+
<small class="text-muted">Specific question sets require ID mapping configuration.</small>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="bg-light p-2 rounded border mb-3">
|
|
106
|
+
<label class="small font-weight-bold">Items to Pickup:</label>
|
|
107
|
+
<% return_authorization.return_items.each do |item| %>
|
|
108
|
+
<div class="d-flex align-items-center mb-1 pl-2">
|
|
109
|
+
<%= icon('cube', class: "text-muted mr-2") %>
|
|
110
|
+
<span class="text-sm font-weight-bold">
|
|
111
|
+
<%= item.inventory_unit.variant.name %>
|
|
112
|
+
</span>
|
|
113
|
+
<span class="badge badge-info ml-auto">Qty: 1</span>
|
|
114
|
+
</div>
|
|
115
|
+
<% end %>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="form-group pt-2 border-top">
|
|
119
|
+
<div class="form-check">
|
|
120
|
+
<input type="checkbox" class="form-check-input" id="confirmCheck-<%= return_authorization.id %>" required>
|
|
121
|
+
<label class="form-check-label text-sm font-weight-bold" for="confirmCheck-<%= return_authorization.id %>">
|
|
122
|
+
I confirm the details are correct.
|
|
123
|
+
</label>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="modal-footer bg-light">
|
|
130
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
|
131
|
+
<%= f.button class: "btn btn-primary ml-2" do %>
|
|
132
|
+
Confirm Pickup
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
135
|
+
<% end %>
|
|
136
|
+
<%# FORM END %>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<%# --- END MODAL --- %>
|
|
141
|
+
|
|
142
|
+
<% end %>
|
|
143
|
+
|
|
144
|
+
<% end %>
|
|
145
|
+
|
|
146
|
+
<%# Existing Edit Button %>
|
|
147
|
+
<%= link_to_edit(
|
|
148
|
+
return_authorization,
|
|
149
|
+
no_text: true,
|
|
150
|
+
data: {
|
|
151
|
+
row_link_target: :link,
|
|
152
|
+
},
|
|
153
|
+
url: edit_link,
|
|
154
|
+
) %>
|
|
155
|
+
</div>
|
|
156
|
+
</td>
|
|
157
|
+
</tr>
|