ultimatepos/Modules/Tms/Services/TmsRouteService.php

163 lines
5.1 KiB
PHP

<?php
namespace Modules\Tms\Services;
use Illuminate\Support\Facades\DB;
use Modules\Tms\Models\RoutePlan;
use Modules\Tms\Models\RouteStop;
use Modules\Tms\Models\Shipment;
use Modules\Tms\Utils\TmsUtil;
class TmsRouteService
{
public function __construct(
protected TmsUtil $tmsUtil
) {}
public function generateRouteRefNo(int $business_id): string
{
$count = RoutePlan::where('business_id', $business_id)->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'));
}
}