diff --git a/public/assets/js/dashboard_map.js b/public/assets/js/dashboard_map.js index 16f8a78d..874883a7 100644 --- a/public/assets/js/dashboard_map.js +++ b/public/assets/js/dashboard_map.js @@ -1,9 +1,8 @@ class DashboardMap { - constructor(options, rider_markers, cust_markers, mc_markers) { + constructor(options, rider_markers, cust_markers) { this.options = options; this.rider_markers = rider_markers; this.cust_markers = cust_markers; - this.mobile_cust_markers = mc_markers; // layer groups this.layer_groups = { @@ -81,6 +80,30 @@ class DashboardMap { } } + switchJobOrderOrigin(jo_id, jo_origin) { + console.log('switching jo ' + jo_id + ' to ' + jo_origin); + + // find the marker + if (this.cust_markers.hasOwnProperty(jo_id)) { + var marker = this.cust_markers[jo_id]; + } else { + console.log('marker not found for customer'); + return true; + } + + // add marker to proper layer group + console.log(jo_origin); + if (jo_origin == 'mobile') { + this.layer_groups.customer.removeLayer(marker); + this.layer_groups.mobile_customer.addLayer(marker); + marker.setIcon(this.options.icons.mobile_customer); + } else { + this.layer_groups.mobile_customer.removeLayer(marker); + this.layer_groups.customer.addLayer(marker); + marker.setIcon(this.options.icons.customer); + } + } + putMarker(id, lat, lng, markers, icon, layer_group, popup_url) { var my = this; // existing marker @@ -106,7 +129,7 @@ class DashboardMap { $.get(url).done(function(data) { popup.setContent(data); popup.update(); - }); + }); }); } } @@ -125,7 +148,6 @@ class DashboardMap { removeCustomerMarker(id) { console.log('removing customer marker for ' + id); - var layer_group = this.layer_groups.customer; var markers = this.cust_markers; // no customer marker with that id @@ -134,7 +156,8 @@ class DashboardMap { return; } - layer_group.removeLayer(markers[id]); + this.layer_groups.customer.removeLayer(markers[id]); + this.layer_groups.mobile_customer.removeLayer(markers[id]); } putMobileCustomerMarker(id, lat, lng) { @@ -142,27 +165,13 @@ class DashboardMap { id, lat, lng, - this.mobile_cust_markers, + this.cust_markers, this.options.icons.mobile_customer, this.layer_groups.mobile_customer, this.options.cust_popup_url ); } - removeMobileCustomerMarker(id) { - console.log('removing mobile customer marker for ' + id); - var layer_group = this.layer_groups.mobile_customer; - var markers = this.mobile_cust_markers; - - // no customer marker with that id - if (!markers.hasOwnProperty(id)) { - console.log('no such marker to remove'); - return; - } - - layer_group.removeLayer(markers[id]); - } - putRiderAvailableMarker(id, lat, lng) { this.putMarker( id, @@ -215,22 +224,16 @@ class DashboardMap { // get riders and job orders var riders = response.riders; var jos = response.jos; - var mobile_jos = response.mobile_jos; // job orders $.each(jos, function(id, data) { var lat = data.latitude; var lng = data.longitude; - my.putCustomerMarker(id, lat, lng); - }); - - // mobile app job orders - $.each(mobile_jos, function(id, data) { - var lat = data.latitude; - var lng = data.longitude; - - my.putMobileCustomerMarker(id, lat, lng); + if (data.is_mobile) + my.putMobileCustomerMarker(id, lat, lng); + else + my.putCustomerMarker(id, lat, lng); }); // riders diff --git a/public/assets/js/map_mqtt.js b/public/assets/js/map_mqtt.js index 8cf3a6af..0e6bdb31 100644 --- a/public/assets/js/map_mqtt.js +++ b/public/assets/js/map_mqtt.js @@ -45,6 +45,10 @@ class MapEventHandler { // subscribe to jo status console.log('subscribing to ' + my.options.channels.jo_status); my.mqtt.subscribe(my.options.channels.jo_status); + + // subscribe to jo origin + console.log('subscribing to ' + my.options.channels.jo_origin); + my.mqtt.subscribe(my.options.channels.jo_origin); } } @@ -134,6 +138,14 @@ class MapEventHandler { this.dashmap.removeCustomerMarker(id); break; } + break; + case "origin": + console.log("got origin for jo " + payload); + if (payload == 'mobile_app') + { + this.dashmap.switchJobOrderOrigin(chan_split[1], 'mobile'); + } + break; } } } diff --git a/src/Controller/APIController.php b/src/Controller/APIController.php index eec92bf7..050d1bc5 100644 --- a/src/Controller/APIController.php +++ b/src/Controller/APIController.php @@ -989,117 +989,209 @@ class APIController extends Controller implements LoggedController $invoice = $ic->generateInvoice($icrit); $jo->setInvoice($invoice); - // assign hub and rider - if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || - ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) + // check if advance order + // check for time when request came in + $request_time_in_int = time(); + $start = '17:00'; + $end = '08:00'; + + $start_time = strtotime($start); + $end_time = strtotime($end); + + if (($request_time_in_int > $start_time) || + (($request_time_in_int < $start_time) && ($request_time_in_int < $end_time))) { - // get nearest hub - // $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im); - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); + // advance order + // assign hub and rider + if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || + ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) + { + // get nearest hub + // $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im); + // TODO: when the inventory manager kicks in, create new function that finds the + // the nearest hub with inventory and rider slot + // convert to DateTime + $nearest_hub = $this->findAdvanceNearestHub($jo, $request_time_in_int, $em, $map_tools); + } + else + { + $nearest_hub = $this->findAdvanceNearestHub($jo, $request_time_in_int, $em, $map_tools); + } + + if (!empty($nearest_hub)) + { + $jo->setHub($nearest_hub); + $jo->setStatus(JOStatus::ASSIGNED); + } + + $em->persist($jo); + $em->persist($invoice); + + // add event log for JO + $event = new JOEvent(); + $event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::CREATE) + ->setJobOrder($jo); + $em->persist($event); + + // check JO status + if ($jo->getStatus() == JOStatus::ASSIGNED) + { + // add event logs for hub and rider assignments + $hub_assign_event = new JOEvent(); + $hub_assign_event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::HUB_ASSIGN) + ->setJobOrder($jo); + + $em->persist($hub_assign_event); + } + + $em->flush(); + + // make invoice json data + $invoice_data = [ + 'total_price' => $invoice->getTotalPrice(), + 'vat_ex_price' => $invoice->getVATExclusivePrice(), + 'vat' => $invoice->getVAT(), + 'discount' => $invoice->getDiscount(), + 'trade_in' => $invoice->getTradeIn(), + ]; + $items = $invoice->getItems(); + $items_data = []; + foreach ($items as $item) + { + $items_data[] = [ + 'title' => $item->getTitle(), + 'qty' => $item->getQuantity() + 0, + 'price' => $item->getPrice() + 0.0, + ]; + } + $invoice_data['items'] = $items_data; + + // make job order data + $data = [ + 'jo_id' => $jo->getID(), + 'invoice' => $invoice_data + ]; + + // set data + $res->setData($data); + } else { - $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); - } - - if (!empty($nearest_hub)) - { - // assign rider - $available_riders = $nearest_hub->getAvailableRiders(); - if (count($available_riders) > 0) + // assign hub and rider + if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || + ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) { - $assigned_rider = null; - if (count($available_riders) > 1) - { - // TODO: the setting of riders into an array - // will no longer be necessary when the contents - // of randomizeRider changes - $riders = []; - foreach ($available_riders as $rider) - { - $riders[] = $rider; - } - - $assigned_rider = $this->randomizeRider($riders); - } - else - $assigned_rider = $available_riders[0]; - - $jo->setHub($nearest_hub); - $jo->setRider($assigned_rider); - $jo->setStatus(JOStatus::ASSIGNED); - - $assigned_rider->setAvailable(false); + // get nearest hub + // $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im); + $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); + } + else + { + $nearest_hub = $this->findNearestHub($jo, $em, $map_tools); } - } - $em->persist($jo); - $em->persist($invoice); + if (!empty($nearest_hub)) + { + // assign rider + $available_riders = $nearest_hub->getAvailableRiders(); + if (count($available_riders) > 0) + { + $assigned_rider = null; + if (count($available_riders) > 1) + { + // TODO: the setting of riders into an array + // will no longer be necessary when the contents + // of randomizeRider changes + $riders = []; + foreach ($available_riders as $rider) + { + $riders[] = $rider; + } - // add event log for JO - $event = new JOEvent(); - $event->setDateHappen(new DateTime()) - ->setTypeID(JOEventType::CREATE) - ->setJobOrder($jo); - $em->persist($event); + $assigned_rider = $this->randomizeRider($riders); + } + else + $assigned_rider = $available_riders[0]; + + $jo->setHub($nearest_hub); + $jo->setRider($assigned_rider); + $jo->setStatus(JOStatus::ASSIGNED); - // check JO status - if ($jo->getStatus() == JOStatus::ASSIGNED) - { - // add event logs for hub and rider assignments - $hub_assign_event = new JOEvent(); - $hub_assign_event->setDateHappen(new DateTime()) - ->setTypeID(JOEventType::HUB_ASSIGN) + $assigned_rider->setAvailable(false); + } + } + + $em->persist($jo); + $em->persist($invoice); + + // add event log for JO + $event = new JOEvent(); + $event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::CREATE) ->setJobOrder($jo); + $em->persist($event); - $em->persist($hub_assign_event); + // check JO status + if ($jo->getStatus() == JOStatus::ASSIGNED) + { + // add event logs for hub and rider assignments + $hub_assign_event = new JOEvent(); + $hub_assign_event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::HUB_ASSIGN) + ->setJobOrder($jo); - $rider_assign_event = new JOEvent(); - $rider_assign_event->setDateHappen(new DateTime()) - ->setTypeID(JOEventType::RIDER_ASSIGN) - ->setJobOrder($jo); + $em->persist($hub_assign_event); - $em->persist($rider_assign_event); + $rider_assign_event = new JOEvent(); + $rider_assign_event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::RIDER_ASSIGN) + ->setJobOrder($jo); - // user mqtt event - $payload = [ - 'event' => 'outlet_assign' + $em->persist($rider_assign_event); + + // user mqtt event + $payload = [ + 'event' => 'outlet_assign' + ]; + $mclient->sendEvent($jo, $payload); + + $rah->assignJobOrder($jo, $jo->getRider()); + } + + $em->flush(); + + // make invoice json data + $invoice_data = [ + 'total_price' => $invoice->getTotalPrice(), + 'vat_ex_price' => $invoice->getVATExclusivePrice(), + 'vat' => $invoice->getVAT(), + 'discount' => $invoice->getDiscount(), + 'trade_in' => $invoice->getTradeIn(), ]; - $mclient->sendEvent($jo, $payload); + $items = $invoice->getItems(); + $items_data = []; + foreach ($items as $item) + { + $items_data[] = [ + 'title' => $item->getTitle(), + 'qty' => $item->getQuantity() + 0, + 'price' => $item->getPrice() + 0.0, + ]; + } + $invoice_data['items'] = $items_data; - $rah->assignJobOrder($jo, $jo->getRider()); - } - - $em->flush(); - - // make invoice json data - $invoice_data = [ - 'total_price' => $invoice->getTotalPrice(), - 'vat_ex_price' => $invoice->getVATExclusivePrice(), - 'vat' => $invoice->getVAT(), - 'discount' => $invoice->getDiscount(), - 'trade_in' => $invoice->getTradeIn(), - ]; - $items = $invoice->getItems(); - $items_data = []; - foreach ($items as $item) - { - $items_data[] = [ - 'title' => $item->getTitle(), - 'qty' => $item->getQuantity() + 0, - 'price' => $item->getPrice() + 0.0, + // make job order data + $data = [ + 'jo_id' => $jo->getID(), + 'invoice' => $invoice_data ]; + + // set data + $res->setData($data); } - $invoice_data['items'] = $items_data; - - // make job order data - $data = [ - 'jo_id' => $jo->getID(), - 'invoice' => $invoice_data - ]; - - // set data - $res->setData($data); return $res->getReturnResponse(); } @@ -2464,4 +2556,306 @@ class APIController extends Controller implements LoggedController return $selected_rider; } + protected function findAdvanceNearestHub($jo, $request_time, EntityManagerInterface $em, MapTools $map_tools) + { + // get the nearest 10 hubs + $selected_hub = null; + $hubs = $map_tools->getClosestOpenHubs($jo->getCoordinates(), 10); + + $nearest_hubs_with_distance = []; + $nearest_branch_codes = []; + foreach ($hubs as $hub) + { + $nearest_hubs_with_distance[] = $hub; + //if (!empty($hub['hub']->getBranchCode())) + // $nearest_branch_codes[] = $hub['hub']->getBranchCode(); + } + + // check if nearest hubs have branch codes + //if (count($nearest_branch_codes) == 0) + // return $selected_hub; + + // assume all 10 have stock + // find the nearest hub with available riders + $nearest = null; + $slot_found = false; + foreach ($nearest_hubs_with_distance as $nhd) + { + if (empty($nearest)) + $nearest = $nhd; + else + { + if ($nhd['distance'] < $nearest['distance']) + $nearest = $nhd; + } + + $result = $this->findRiderSlots($jo, $nearest['hub'], $request_time, $em); + + if ($result != null) + $slot_found = $result['slot_found']; + else + $nearest = null; + } + + if ($slot_found && $nearest != null) + { + $result = $this->findRiderSlots($jo, $nearest['hub'], $request_time, $em); + + if ($result != null) + { + $jo_date_schedule = $result['date_schedule']; + + $jo->setAdvanceOrder(true); + $jo->setDateSchedule($jo_date_schedule); + + $selected_hub = $nearest['hub']; + } + + } + + // uncomment this snippet when inventory check becomes active + // get battery sku + /* + if ($batt != null) + { + $skus[] = $batt->getSAPCode(); + + // api call to check inventory + // pass the list of branch codes of nearest hubs and the skus + // go through returned list of branch codes + // bypass inventory check for now + // $hubs_with_inventory = $im->getBranchesInventory($nearest_branch_codes, $skus); + if (!empty($hubs_with_inventory)) + { + $nearest = []; + $flag_hub_found = false; + foreach ($hubs_with_inventory as $hub_with_inventory) + { + // find hub according to branch code + $found_hub = $em->getRepository(Hub::class)->findOneBy(['branch_code' => $hub_with_inventory['BranchCode']]); + if ($found_hub != null) + { + // check rider availability + if (count($found_hub->getAvailableRiders()) > 0) + { + // check against nearest hubs with distance + foreach ($nearest_hubs_with_distance as $nhd) + { + // get distance of hub from location, compare with $nearest. if less, replace nearest + if ($found_hub->getID() == $nhd['hub']->getID()) + { + if (empty($nearest)) + { + $nearest = $nhd; + $flag_hub_found = true; + } + else + { + if ($nhd['distance'] < $nearest['distance']) + { + $nearest = $nhd; + $flag_hub_found = true; + } + } + } + } + } + } + } + $selected_hub = $nearest['hub']; + } + } */ + return $selected_hub; + } + + protected function findRiderSlots($jo, Hub $hub, $request_time, EntityManagerInterface $em) + { + $flag_found_slot = false; + $results = []; + + // array of # of riders that can handle JOs in a timeslot + $hub_rider_slots = []; + + // populate the array with the hub's rider slots + for ($i = 0; $i <=7; $i++) + { + $hub_rider_slots[$i] = $hub->getRiderSlots(); + } + + // check hub's advance orders for the day + // get number of advance orders for the next day if request came in before midnight + // or for current day if request came in after midnight + // check request_time + $midnight = strtotime('00:00'); + $start_date = new DateTime(); + $end_date = new DateTime(); + + if ($request_time < $midnight) + { + // add +1 to date to start and end date + $start_date->add(new DateInterval('P1D')); + $end_date->add(new DateInterval('P1D')); + } + + // set time bounds for the start and end date + $start_date->setTime(0, 1); + $end_date->setTime(23, 59); + + $str_request_time = date('Y-m-d H:i:s', $request_time); + $time_of_request = DateTime::createFromFormat('Y-m-d H:i:s', $str_request_time); + + // NOTE: get advance orders via query + // get JOs assigned to hub that are advance orders and scheduled for the next day(if before midnight) or current day(if after midnight) with + // for hub assignment status + $query = $em->createQuery('select jo from App\Entity\JobOrder jo where jo.hub = :hub and jo.flag_advance = true and + jo.date_schedule >= :date_start and jo.date_schedule <= :date_end and jo.status = :status'); + $jos_advance_orders = $query->setParameters([ + 'hub' => $hub, + 'date_start' => $start_date, + 'date_end' => $end_date, + 'status' => JOStatus::ASSIGNED, + ]) + ->getResult(); + + // check each JO's date_schedule, decrement rider_slots if date schedule falls in that slot + // index - equivalent time + // 0 - 8-9 + // 1 - 9-10 + // 2 - 10-11 + // 3 - 11-12 + // 4 - 12 -13 + // 5 - 13-14 + // 6 - 14-15 + // 7 - 15-16 + foreach ($jos_advance_orders as $advance_jo) + { + // get time schedule + $date_schedule = $advance_jo->getDateSchedule(); + $time_schedule = $date_schedule->format('H:i'); + + $hour_schedule = $date_schedule->format('H'); + $minute_schedule = $date_schedule->format('i'); + + // check minutes. for now, if not 00, take up current slot and next one + // if hour fits in the last slot but minutes is not 00, move to the next available hub + // TODO: need to add custom minutes threshold + if ($minute_schedule != '00') + { + switch($hour_schedule) { + case '8': + $hub_rider_slots[0]--; + $hub_rider_slots[1]--; + break; + case '9': + $hub_rider_slots[1]--; + $hub_rider_slots[2]--; + break; + case '10': + $hub_rider_slots[2]--; + $hub_rider_slots[3]--; + break; + case '11': + $hub_rider_slots[3]--; + $hub_rider_slots[4]--; + break; + case '12': + $hub_rider_slots[4]--; + $hub_rider_slots[5]--; + break; + case '13': + $hub_rider_slots[5]--; + $hub_rider_slots[6]--; + break; + case '14': + $hub_rider_slots[6]--; + $hub_rider_slots[7]--; + break; + case '15': + error_log('No slots for the day'); + break; + default: + error_log('Does not fit in any time slot. ' . $time_schedule); + } + } + else + { + switch ($hour_schedule) { + case '8': + $hub_rider_slots[0]--; + break; + case '9': + $hub_rider_slots[1]--; + break; + case '10': + $hub_rider_slots[2]--; + break; + case '11': + $hub_rider_slots[3]--; + break; + case '12': + $hub_rider_slots[4]--; + break; + case '13': + $hub_rider_slots[5]--; + break; + case '14': + $hub_rider_slots[6]--; + break; + case '15': + $hub_rider_slots[7]--; + break; + default: + error_log('Does not fit in any time slot.' . $time_schedule); + } + } + + } + + // find first free slot + foreach ($hub_rider_slots as $index => $rider_slot) + { + if ($rider_slot > 0) + { + $flag_found_slot = true; + + $jo_date_schedule = $start_date; + switch ($index) { + case '0': + $jo_date_schedule->setTime(8,0); + break; + case '1': + $jo_date_schedule->setTime(9,0); + break; + case '2': + $jo_date_schedule->setTime(10,0); + break; + case '3': + $jo_date_schedule->setTime(11,0); + break; + case '4': + $jo_date_schedule->setTime(12,0); + break; + case '5': + $jo_date_schedule->setTime(13,0); + break; + case '6': + $jo_date_schedule->setTime(14,0); + break; + case '7': + $jo_date_schedule->setTime(15,0); + break; + default: + error_log('Cannot find time slot.'); + } + + $results = [ + 'slot_found' => $flag_found_slot, + 'date_schedule' => $jo_date_schedule, + ]; + + break; + } + } + return $results; + } } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index cbc4afa7..1a157b78 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -65,8 +65,7 @@ class HomeController extends Controller $riders[$rider_id]['has_jo'] = true; } - // get JOs with transaction origin TransactionOrigin::MOBILE_APP from list of active_jos - $mobile_jos = []; + // get active JOs and check transaction origin for TransactionOrigin::MOBILE_APP foreach ($active_jos as $jo_id => $jo_data) { $jo = $em->getRepository(JobOrder::class)->find($jo_id); @@ -78,8 +77,11 @@ class HomeController extends Controller if ($jo->getSource() == TransactionOrigin::MOBILE_APP) { - $mobile_jos[$jo_id] = $jo_data; - unset($active_jos[$jo_id]); + $active_jos[$jo_id]['is_mobile'] = true; + } + else + { + $active_jos[$jo_id]['is_mobile'] = false; } } @@ -140,7 +142,6 @@ class HomeController extends Controller return $this->json([ 'jos' => $active_jos, 'riders' => $riders, - 'mobile_jos' => $mobile_jos, ]); } diff --git a/src/Controller/HubController.php b/src/Controller/HubController.php index d6334c80..5c8adb04 100644 --- a/src/Controller/HubController.php +++ b/src/Controller/HubController.php @@ -152,7 +152,8 @@ class HubController extends Controller ->setTimeClose($time_close) ->setCoordinates($point) ->setBranchCode($req->request->get('branch_code', '')) - ->setStatusOpen($req->request->get('status_open') ?? false); + ->setStatusOpen($req->request->get('status_open') ?? false) + ->setRiderSlots($req->request->get('rider_slots', 0)); } protected function setQueryFilters($datatable, QueryBuilder $query) diff --git a/src/Entity/Hub.php b/src/Entity/Hub.php index f92a424e..9edcb88a 100644 --- a/src/Entity/Hub.php +++ b/src/Entity/Hub.php @@ -56,6 +56,12 @@ class Hub */ protected $status_open; + // number of rider slots per day + /** + * @ORM\Column(type="integer") + */ + protected $rider_slots; + public function __construct() { $this->time_open = new DateTime(); @@ -150,4 +156,15 @@ class Hub return $this->status_open; } + public function setRiderSlots($rider_slots) + { + $this->rider_slots = $rider_slots; + return $this; + } + + public function getRiderSlots() + { + return $this->rider_slots; + } + } diff --git a/src/EventListener/JobOrderActiveCacheListener.php b/src/EventListener/JobOrderActiveCacheListener.php index 89eedd01..05a6a612 100644 --- a/src/EventListener/JobOrderActiveCacheListener.php +++ b/src/EventListener/JobOrderActiveCacheListener.php @@ -88,7 +88,13 @@ class JobOrderActiveCacheListener // send jo location $this->mqtt->publish( 'jo/' . $jo->getID() . '/location', - $coords->getLatitude() . ':' . $coords->getLongitude() + $coords->getLatitude() . ':' . $coords->getLongitude() + ); + + // send transaction origin + $this->mqtt->publish( + 'jo/' . $jo->getID() . '/origin', + $jo->getSource() ); // TODO: do we still need to send jo status? diff --git a/templates/home.html.twig b/templates/home.html.twig index c9e4953e..61b93de0 100644 --- a/templates/home.html.twig +++ b/templates/home.html.twig @@ -20,7 +20,7 @@ {% endif %}