count() + 1; return 'RT-'.str_pad((string) $count, 5, '0', STR_PAD_LEFT); } public function createRoute(int $business_id, array $data): RoutePlan { return RoutePlan::create(array_merge($data, [ 'business_id' => $business_id, 'ref_no' => $this->generateRouteRefNo($business_id), 'created_by' => auth()->id(), ])); } public function addShipmentsToRoute(RoutePlan $route, array $shipment_ids): void { $shipments = Shipment::where('business_id', $route->business_id) ->whereIn('id', $shipment_ids) ->get(); $maxOrder = (int) $route->stops()->max('stop_order'); foreach ($shipments as $shipment) { if ($this->isShipmentBlockedForRoute($shipment)) { continue; } $maxOrder++; RouteStop::create([ 'route_id' => $route->id, 'shipment_id' => $shipment->id, 'stop_order' => $maxOrder, 'latitude' => $shipment->destination_latitude, 'longitude' => $shipment->destination_longitude, 'address' => $shipment->destination_address, 'status' => 'pending', ]); $shipment->update(['route_id' => $route->id]); } } /** * Nearest-neighbor route optimization from origin. */ public function optimizeStops(RoutePlan $route): RoutePlan { $stops = $route->stops()->with('shipment')->get(); if ($stops->count() < 2) { return $route; } $originLat = $route->origin_latitude; $originLng = $route->origin_longitude; if (! $originLat || ! $originLng) { $first = $stops->first(fn ($s) => $s->latitude && $s->longitude); if ($first) { $originLat = $first->latitude; $originLng = $first->longitude; } } if (! $originLat || ! $originLng) { return $route; } $remaining = $stops->filter(fn ($s) => $s->latitude && $s->longitude)->values(); $ordered = []; $currentLat = (float) $originLat; $currentLng = (float) $originLng; $totalDistance = 0.0; while ($remaining->isNotEmpty()) { $nearestIdx = 0; $nearestDist = PHP_FLOAT_MAX; foreach ($remaining as $idx => $stop) { $dist = $this->haversineKm($currentLat, $currentLng, (float) $stop->latitude, (float) $stop->longitude); if ($dist < $nearestDist) { $nearestDist = $dist; $nearestIdx = $idx; } } $next = $remaining->pull($nearestIdx); $totalDistance += $nearestDist; $currentLat = (float) $next->latitude; $currentLng = (float) $next->longitude; $ordered[] = $next; } $noCoords = $stops->filter(fn ($s) => ! $s->latitude || ! $s->longitude); foreach ($noCoords as $stop) { $ordered[] = $stop; } DB::transaction(function () use ($ordered, $route, $totalDistance) { foreach ($ordered as $order => $stop) { $stop->update(['stop_order' => $order + 1]); } $route->update([ 'total_distance_km' => round($totalDistance, 2), 'status' => 'optimized', ]); }); return $route->fresh(['stops.shipment']); } public function haversineKm(float $lat1, float $lng1, float $lat2, float $lng2): float { $earthRadius = 6371; $dLat = deg2rad($lat2 - $lat1); $dLng = deg2rad($lng2 - $lng1); $a = sin($dLat / 2) ** 2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2; return $earthRadius * 2 * atan2(sqrt($a), sqrt(1 - $a)); } public function routeStatuses(): array { return [ 'draft' => __('tms::lang.route_status_draft'), 'optimized' => __('tms::lang.route_status_optimized'), 'in_progress' => __('tms::lang.route_status_in_progress'), 'completed' => __('tms::lang.route_status_completed'), 'cancelled' => __('tms::lang.route_status_cancelled'), ]; } protected function isShipmentBlockedForRoute(Shipment $shipment): bool { $settings = $this->tmsUtil->getTmsSettings($shipment->business_id); if (empty($settings['exclude_uncleared_from_routes'])) { return false; } return app(TmsCustomsShipmentSyncService::class)->customsBlocksTransit($shipment->loadMissing('customsClearance')); } }