Skip to content

Commit 84d1a8a

Browse files
committed
refac: better reordering code
1 parent 6153ff8 commit 84d1a8a

File tree

3 files changed

+106
-93
lines changed

3 files changed

+106
-93
lines changed

backend/trips/services.py

Lines changed: 89 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from django.core.exceptions import ValidationError
2-
from django.db import models
2+
from django.db import transaction, models
33
from typing import List, Dict, Any
44
from .models import Trip, TripStop
55
from orders.models import Stop, Order
66

77

88
class TripValidationError(ValidationError):
99
"""Custom validation error for trip-related violations"""
10+
1011
pass
1112

1213

@@ -50,7 +51,7 @@ def validate_new_trip_stop(trip: Trip, stop: Stop) -> None:
5051
existing_stop_ids = set(ts.stop.id for ts in trip_stops)
5152

5253
# Get the paired stop (pickup if this is delivery, delivery if this is pickup)
53-
paired_stop_type = 'pickup' if stop.stop_type == 'delivery' else 'delivery'
54+
paired_stop_type = "pickup" if stop.stop_type == "delivery" else "delivery"
5455
paired_stop = order.stops.filter(stop_type=paired_stop_type).first()
5556

5657
if not paired_stop:
@@ -78,25 +79,25 @@ def get_incomplete_orders(trip: Trip) -> List[Order]:
7879
Returns:
7980
List of Order instances that are missing either pickup or delivery stops
8081
"""
81-
trip_stops = trip.trip_stops.select_related('stop', 'stop__order').all()
82+
trip_stops = trip.trip_stops.select_related("stop", "stop__order").all()
8283
orders_in_trip = {}
8384

8485
# Group stops by order
8586
for trip_stop in trip_stops:
8687
if trip_stop.stop.order:
8788
order = trip_stop.stop.order
8889
if order.id not in orders_in_trip:
89-
orders_in_trip[order.id] = {'order': order, 'stops': []}
90-
orders_in_trip[order.id]['stops'].append(trip_stop.stop)
90+
orders_in_trip[order.id] = {"order": order, "stops": []}
91+
orders_in_trip[order.id]["stops"].append(trip_stop.stop)
9192

9293
incomplete_orders = []
9394
for order_data in orders_in_trip.values():
94-
order = order_data['order']
95-
stops = order_data['stops']
95+
order = order_data["order"]
96+
stops = order_data["stops"]
9697
stop_types = {stop.stop_type for stop in stops}
9798

9899
# Check if both pickup and delivery are present
99-
if 'pickup' not in stop_types or 'delivery' not in stop_types:
100+
if "pickup" not in stop_types or "delivery" not in stop_types:
100101
incomplete_orders.append(order)
101102

102103
return incomplete_orders
@@ -116,8 +117,8 @@ def ensure_order_pair_in_trip(trip: Trip, order: Order) -> Dict[str, Any]:
116117
Raises:
117118
TripValidationError: If the order doesn't have both pickup and delivery stops
118119
"""
119-
pickup_stop = order.stops.filter(stop_type='pickup').first()
120-
delivery_stop = order.stops.filter(stop_type='delivery').first()
120+
pickup_stop = order.stops.filter(stop_type="pickup").first()
121+
delivery_stop = order.stops.filter(stop_type="delivery").first()
121122

122123
if not pickup_stop:
123124
raise TripValidationError(
@@ -129,13 +130,12 @@ def ensure_order_pair_in_trip(trip: Trip, order: Order) -> Dict[str, Any]:
129130
f"Order {order.order_number} does not have a delivery stop."
130131
)
131132

132-
return {
133-
'pickup_stop': pickup_stop,
134-
'delivery_stop': delivery_stop
135-
}
133+
return {"pickup_stop": pickup_stop, "delivery_stop": delivery_stop}
136134

137135

138-
def add_order_to_trip(trip: Trip, order: Order, pickup_time, delivery_time, notes: str = "") -> Dict[str, Any]:
136+
def add_order_to_trip(
137+
trip: Trip, order: Order, pickup_time, delivery_time, notes: str = ""
138+
) -> Dict[str, Any]:
139139
"""
140140
Add both pickup and delivery stops for an order to a trip atomically.
141141
@@ -157,14 +157,17 @@ def add_order_to_trip(trip: Trip, order: Order, pickup_time, delivery_time, note
157157

158158
# Validate the order has both stops
159159
order_stops = ensure_order_pair_in_trip(trip, order)
160-
pickup_stop = order_stops['pickup_stop']
161-
delivery_stop = order_stops['delivery_stop']
160+
pickup_stop = order_stops["pickup_stop"]
161+
delivery_stop = order_stops["delivery_stop"]
162162

163163
with transaction.atomic():
164164
# Get the next available sequence numbers for the trip
165-
last_sequence = trip.trip_stops.aggregate(
166-
max_sequence=models.Max('sequence')
167-
)['max_sequence'] or 0
165+
last_sequence = (
166+
trip.trip_stops.aggregate(max_sequence=models.Max("sequence"))[
167+
"max_sequence"
168+
]
169+
or 0
170+
)
168171

169172
pickup_sequence = last_sequence + 1
170173
delivery_sequence = last_sequence + 2
@@ -175,7 +178,7 @@ def add_order_to_trip(trip: Trip, order: Order, pickup_time, delivery_time, note
175178
stop=pickup_stop,
176179
sequence=pickup_sequence,
177180
planned_arrival_time=pickup_time,
178-
notes=notes or f'Pickup for {order.order_number}'
181+
notes=notes or f"Pickup for {order.order_number}",
179182
)
180183
pickup_trip_stop.save(skip_validation=True)
181184

@@ -184,13 +187,13 @@ def add_order_to_trip(trip: Trip, order: Order, pickup_time, delivery_time, note
184187
stop=delivery_stop,
185188
sequence=delivery_sequence,
186189
planned_arrival_time=delivery_time,
187-
notes=notes or f'Delivery for {order.order_number}'
190+
notes=notes or f"Delivery for {order.order_number}",
188191
)
189192
delivery_trip_stop.save(skip_validation=True)
190193

191194
return {
192-
'pickup_trip_stop': pickup_trip_stop,
193-
'delivery_trip_stop': delivery_trip_stop
195+
"pickup_trip_stop": pickup_trip_stop,
196+
"delivery_trip_stop": delivery_trip_stop,
194197
}
195198

196199

@@ -204,38 +207,46 @@ def validate_pickup_before_delivery(trip: Trip) -> None:
204207
Raises:
205208
TripValidationError: If any order has delivery before pickup
206209
"""
207-
trip_stops = trip.trip_stops.select_related('stop', 'stop__order').order_by('sequence')
210+
trip_stops = trip.trip_stops.select_related("stop", "stop__order").order_by(
211+
"sequence"
212+
)
208213
orders_in_trip = {}
209214

210215
# Group stops by order and track their positions
211216
for trip_stop in trip_stops:
212217
if trip_stop.stop.order:
213218
order = trip_stop.stop.order
214219
if order.id not in orders_in_trip:
215-
orders_in_trip[order.id] = {'order': order, 'stops': []}
216-
orders_in_trip[order.id]['stops'].append({
217-
'trip_stop': trip_stop,
218-
'stop_type': trip_stop.stop.stop_type,
219-
'sequence_position': trip_stop.sequence
220-
})
220+
orders_in_trip[order.id] = {"order": order, "stops": []}
221+
orders_in_trip[order.id]["stops"].append(
222+
{
223+
"trip_stop": trip_stop,
224+
"stop_type": trip_stop.stop.stop_type,
225+
"sequence_position": trip_stop.sequence,
226+
}
227+
)
221228

222229
# Check each order's pickup-delivery ordering
223230
for order_data in orders_in_trip.values():
224-
order = order_data['order']
225-
stops = order_data['stops']
231+
order = order_data["order"]
232+
stops = order_data["stops"]
226233

227-
pickup_positions = [s['sequence_position'] for s in stops if s['stop_type'] == 'pickup']
228-
delivery_positions = [s['sequence_position'] for s in stops if s['stop_type'] == 'delivery']
234+
pickup_positions = [
235+
s["sequence_position"] for s in stops if s["stop_type"] == "pickup"
236+
]
237+
delivery_positions = [
238+
s["sequence_position"] for s in stops if s["stop_type"] == "delivery"
239+
]
229240

230241
# If both pickup and delivery exist, pickup must come before delivery
231242
if pickup_positions and delivery_positions:
232-
min_pickup_pos = min(pickup_positions)
233-
min_delivery_pos = min(delivery_positions)
243+
max_pickup_pos = max(pickup_positions)
244+
max_delivery_pos = max(delivery_positions)
234245

235-
if min_pickup_pos >= min_delivery_pos:
246+
if max_pickup_pos >= max_delivery_pos:
236247
raise TripValidationError(
237-
f"Order {order.order_number} has delivery stop (position {min_delivery_pos}) "
238-
f"before or at same position as pickup stop (position {min_pickup_pos}). "
248+
f"Order {order.order_number} has delivery stop (position {max_delivery_pos}) "
249+
f"before or at same position as pickup stop (position {max_pickup_pos}). "
239250
f"Pickup must occur before delivery."
240251
)
241252

@@ -250,11 +261,47 @@ def get_orders_requiring_both_stops() -> List[Order]:
250261
"""
251262
orders_with_both_stops = []
252263

253-
for order in Order.objects.prefetch_related('stops').all():
264+
for order in Order.objects.prefetch_related("stops").all():
254265
stops = order.stops.all()
255266
stop_types = {stop.stop_type for stop in stops}
256267

257-
if 'pickup' in stop_types and 'delivery' in stop_types:
268+
if "pickup" in stop_types and "delivery" in stop_types:
258269
orders_with_both_stops.append(order)
259270

260271
return orders_with_both_stops
272+
273+
274+
def update_trip_stop_sequences(trip: Trip, new_sequences: List[Dict[str, Any]]) -> None:
275+
"""
276+
Update the sequence order of trip stops.
277+
278+
Args:
279+
trip: The Trip instance to update
280+
new_sequences: List of dicts with 'id' and 'sequence' keys
281+
282+
Raises:
283+
TripValidationError: If some trip stops don't belong to this trip
284+
"""
285+
286+
with transaction.atomic():
287+
# Validate that all provided stop IDs belong to this trip
288+
provided_stop_ids = [item["id"] for item in new_sequences]
289+
existing_stops = TripStop.objects.filter(trip=trip, id__in=provided_stop_ids)
290+
291+
if len(existing_stops) != len(provided_stop_ids):
292+
raise TripValidationError("Some trip stops do not belong to this trip")
293+
294+
# First, set all orders to very high values to avoid constraint conflicts
295+
# Use values starting from 10000 to avoid conflicts with existing orders
296+
for i, sequence_item in enumerate(new_sequences):
297+
trip_stop = TripStop.objects.get(id=sequence_item["id"], trip=trip)
298+
trip_stop.sequence = 10000 + i # Use high values temporarily
299+
trip_stop.save()
300+
301+
# Then set the final order values
302+
for sequence_item in new_sequences:
303+
trip_stop = TripStop.objects.get(id=sequence_item["id"], trip=trip)
304+
sequence_value = sequence_item.get("sequence")
305+
if sequence_value is not None:
306+
trip_stop.sequence = sequence_value
307+
trip_stop.save()

backend/trips/views.py

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from django.db import transaction
21
import logging
32
from django.http import JsonResponse
43
from django.views.decorators.csrf import csrf_exempt
@@ -8,7 +7,12 @@
87
import json
98
from datetime import datetime
109
from .models import Trip, TripStop
11-
from .services import add_order_to_trip, validate_pickup_before_delivery, TripValidationError
10+
from .services import (
11+
add_order_to_trip,
12+
validate_pickup_before_delivery,
13+
update_trip_stop_sequences,
14+
TripValidationError,
15+
)
1216
from orders.models import Order
1317

1418
logger = logging.getLogger(__name__)
@@ -504,8 +508,7 @@ def post(self, request, trip_pk):
504508
"""Reorder trip stops for a given trip"""
505509
try:
506510
data = json.loads(request.body)
507-
# Support both 'orders' (legacy) and 'sequences' (new) parameter names
508-
new_sequences = data.get("sequences") or data.get("orders", [])
511+
new_sequences = data.get("sequences")
509512

510513
logger.info(f"Reordering stops for trip {trip_pk}: {new_sequences}")
511514

@@ -518,36 +521,12 @@ def post(self, request, trip_pk):
518521
except Trip.DoesNotExist:
519522
return JsonResponse({"error": "Trip not found"}, status=404)
520523

521-
with transaction.atomic():
522-
# Validate that all provided stop IDs belong to this trip
523-
provided_stop_ids = [item["id"] for item in new_sequences]
524-
existing_stops = TripStop.objects.filter(
525-
trip=trip, id__in=provided_stop_ids
526-
)
527-
528-
if len(existing_stops) != len(provided_stop_ids):
529-
return JsonResponse(
530-
{"error": "Some trip stops do not belong to this trip"},
531-
status=400,
532-
)
533-
534-
# First, set all orders to very high values to avoid constraint conflicts
535-
# Use values starting from 10000 to avoid conflicts with existing orders
536-
for i, sequence_item in enumerate(new_sequences):
537-
trip_stop = TripStop.objects.get(id=sequence_item["id"], trip=trip)
538-
trip_stop.sequence = 10000 + i # Use high values temporarily
539-
trip_stop.save()
540-
541-
# Then set the final order values
542-
for sequence_item in new_sequences:
543-
trip_stop = TripStop.objects.get(id=sequence_item["id"], trip=trip)
544-
# Support both 'sequence' (new) and 'order' (legacy) field names in data
545-
new_seq = sequence_item.get("sequence") or sequence_item.get("order")
546-
trip_stop.sequence = new_seq
547-
trip_stop.save()
548-
549-
# Validate that pickups happen before deliveries
524+
# Update trip stop sequence
525+
try:
526+
update_trip_stop_sequences(trip, new_sequences)
550527
validate_pickup_before_delivery(trip)
528+
except TripValidationError as e:
529+
return JsonResponse({"error": str(e)}, status=400)
551530

552531
# Return updated trip stops
553532
updated_stops = TripStop.objects.filter(trip=trip).order_by("sequence")

frontend/src/components/trips/TripDetailsDrawer.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,8 @@ export const TripDetailsDrawer: React.FC<TripDetailsDrawerProps> = ({
228228

229229
// Create a copy of the trip stops array
230230
const reorderedStops = Array.from(tripStops)
231-
const [removed] = reorderedStops.splice(sourceIndex, 1)
232-
reorderedStops.splice(destinationIndex, 0, removed)
231+
const removed = reorderedStops.splice(sourceIndex, 2)
232+
reorderedStops.splice(destinationIndex, 0, removed[1])
233233

234234
// Update local state immediately for better UX
235235
setTripStops(reorderedStops)
@@ -240,16 +240,14 @@ export const TripDetailsDrawer: React.FC<TripDetailsDrawerProps> = ({
240240
// Create the sequences array for the API call
241241
const sequences = reorderedStops.map((stop, index) => ({
242242
id: stop.id,
243-
sequence: index + 1, // API expects 1-based sequencing
243+
sequence: index,
244244
}))
245245

246246
// Call the reorder API
247-
await post(`/trips/${trip.id}/reorder-stops/`, {
247+
const stops: TripStop[] = await post(`/trips/${trip.id}/reorder-stops/`, {
248248
sequences,
249249
})
250-
251-
// Refresh trip details to ensure we have the latest data
252-
await fetchTripDetails()
250+
setTripStops(stops)
253251

254252
toast({
255253
title: 'Stops reordered',
@@ -264,17 +262,6 @@ export const TripDetailsDrawer: React.FC<TripDetailsDrawerProps> = ({
264262
}
265263
} catch (err) {
266264
console.error('Error reordering stops:', err)
267-
const errorMessage =
268-
(err as { response?: { data?: { error?: string } } }).response?.data
269-
?.error || 'Failed to reorder stops'
270-
271-
toast({
272-
title: 'Error reordering stops',
273-
description: errorMessage,
274-
status: 'error',
275-
duration: 5000,
276-
isClosable: true,
277-
})
278265

279266
// Revert to original order on error
280267
await fetchTripDetails()

0 commit comments

Comments
 (0)