diff --git a/config/acl.yaml b/config/acl.yaml index c6ccc243..bfd6c6cc 100644 --- a/config/acl.yaml +++ b/config/acl.yaml @@ -264,6 +264,8 @@ access_keys: label: Walk-in - id: jo_walkin.edit label: Walk-in Edit + - id: jo_autoassign.test + label: Autoassign Test - id: support label: Customer Support Access diff --git a/config/menu.yaml b/config/menu.yaml index b0096cf5..45ceab40 100644 --- a/config/menu.yaml +++ b/config/menu.yaml @@ -122,6 +122,10 @@ main_menu: acl: jo_all.list label: View All parent: joborder + - id: jo_autoassign + acl: jo_autoassign.test + label: Autoassign Test + parent: joborder - id: support acl: support.menu diff --git a/config/routes/job_order.yaml b/config/routes/job_order.yaml index d5cd8dfc..f239402d 100644 --- a/config/routes/job_order.yaml +++ b/config/routes/job_order.yaml @@ -226,3 +226,12 @@ jo_walkin_edit_submit: controller: App\Controller\JobOrderController::walkInEditSubmit methods: [POST] +jo_autoassign: + path: /job-order/autoassign + controller: App\Controller\JobOrderController::autoAssignForm + methods: [GET] + +jo_autoassign_test_submit: + path: /job-order/autoassign + controller: App\Controller\JobOrderController::autoAssignSubmit + methods: [POST] diff --git a/src/Controller/JobOrderController.php b/src/Controller/JobOrderController.php index 1d1d84c1..cdb2d665 100644 --- a/src/Controller/JobOrderController.php +++ b/src/Controller/JobOrderController.php @@ -5,6 +5,7 @@ namespace App\Controller; use App\Ramcar\JOStatus; use App\Ramcar\InvoiceCriteria; use App\Ramcar\CMBServiceType; +use App\Ramcar\ServiceType; use App\Entity\CustomerVehicle; use App\Entity\Promo; @@ -96,7 +97,9 @@ class JobOrderController extends Controller $this->denyAccessUnlessGranted('jo_open.edit', null, 'No access.'); $error_array = []; - $error_array = $jo_handler->generateJobOrder($req, $id); + $result = $jo_handler->generateJobOrder($req, $id); + + $error_array = $result['error_array']; // check if any errors were found if (!empty($error_array)) { @@ -145,7 +148,9 @@ class JobOrderController extends Controller // initialize error list $error_array = []; $id = -1; - $error_array = $jo_handler->generateJobOrder($req, $id); + $result = $jo_handler->generateJobOrder($req, $id); + + $error_array = $result['error_array']; // check if any errors were found if (!empty($error_array)) { @@ -1053,5 +1058,160 @@ class JobOrderController extends Controller ]); } + /** + * @Menu(selected="jo_autoassign") + */ + public function autoAssignForm(GISManagerInterface $gis, JobOrderHandlerInterface $jo_handler) + { + $this->denyAccessUnlessGranted('jo_autoassign.test', null, 'No access.'); + $params = $jo_handler->initializeIncomingForm(); + + $params['submit_url'] = $this->generateUrl('jo_autoassign_test_submit'); + $params['return_url'] = $this->generateUrl('jo_autoassign'); + $params['map_js_file'] = $gis->getJSJOFile(); + + $template = 'job-order/autoassign.form.html.twig'; + + // response + return $this->render($template, $params); + } + + public function autoAssignSubmit(Request $req, JobOrderHandlerInterface $jo_handler, + EntityManagerInterface $em, MapTools $map_tools) + { + $this->denyAccessUnlessGranted('jo_autoassign.test', null, 'No access.'); + + // initialize error list + $error_array = []; + $id = -1; + $result = $jo_handler->generateJobOrder($req, $id); + + $error_array = $result['error_array']; + + // check if any errors were found + if (!empty($error_array)) { + // return validation failure response + return $this->json([ + 'success' => false, + 'errors' => $error_array + ], 422); + } + + // get job order + $jo = $result['job_order']; + + if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) || + ($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY)) + $this->autoAssignHubAndRider($jo, $em, $map_tools); + + + // return successful response + return $this->json([ + 'success' => 'Changes have been saved!' + ]); + + } + + protected function autoAssignHubAndRider($jo, EntityManagerInterface $em, MapTools $map_tools) + { + // get the nearest 10 hubs + // TODO: move this snippet to a function + $hubs = $map_tools->getClosestHubs($jo->getCoordinates(), 10, date("H:i:s")); + + $nearest_hubs = []; + $nearest_hubs_with_distance = []; + + foreach ($hubs as $hub) + { + $nearest_hubs_with_distance[] = $hub; + $nearest_hubs[] = $hub['hub']; + } + + // get battery sku + $invoice = $jo->getInvoice(); + if ($invoice != null) + { + $items = $invoice->getItems(); + $sku = ''; + foreach ($items as $item) + { + $sku = $item->getBattery()->getSAPCode(); + } + + // api call to check inventory + // pass the list of nearest hubs and the sku + // go through returned list of hubs for available riders + // for now, use all hubs + $hubs_with_inventory = $em->getRepository(Hub::class)->findAll(); + $nearest = []; + foreach ($hubs_with_inventory as $hub_with_inventory) + { + // check rider availability + if (count($hub_with_inventory->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 ($hub_with_inventory->getID() == $nhd['hub']->getID()) + { + if (empty($nearest)) + { + $nearest = $nhd; + } + else + { + if ($nhd['distance'] < $nearest['distance']) + $nearest = $nhd; + } + } + } + } + } + + // return $nearest + error_log('Nearest Hub ' . $nearest['hub']->getID()); + error_log('With distance ' . $nearest['distance']); + + $jo->setHub($nearest['hub']); + + $hub_riders = $nearest['hub']->getAvailableRiders(); + $rider = null; + if (count($hub_riders) > 1) + { + // this will no longer be necessary when the contents + // of randomizeRider changes + $available_riders = []; + foreach ($hub_riders as $rider) + { + $available_riders[] = $rider; + } + + $rider = $this->randomizeRider($available_riders); + } + else + $rider = $hub_riders[0]; + + $jo->setRider($rider); + + $jo->setStatus(JOStatus::ASSIGNED); + + $em->persist($jo); + $em->flush(); + + } + } + + protected function randomizeRider($riders) + { + // TODO: get redis to track the sales per rider per day + // check the time they came in + // for now, randomize the rider + $selected_index = array_rand($riders); + + $selected_rider = $riders[$selected_index]; + + return $selected_rider; + } } diff --git a/src/Service/JobOrderHandler/ResqJobOrderHandler.php b/src/Service/JobOrderHandler/ResqJobOrderHandler.php index a12e09d5..deeea114 100644 --- a/src/Service/JobOrderHandler/ResqJobOrderHandler.php +++ b/src/Service/JobOrderHandler/ResqJobOrderHandler.php @@ -405,7 +405,14 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface } } - return $error_array; + $data['error_array'] = $error_array; + + if ($jo != null) + { + $data['job_order'] = $jo; + } + + return $data; } // dispatch job order diff --git a/templates/job-order/autoassign.form.html.twig b/templates/job-order/autoassign.form.html.twig new file mode 100644 index 00000000..bba70b39 --- /dev/null +++ b/templates/job-order/autoassign.form.html.twig @@ -0,0 +1,1774 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Job Order

+
+
+
+ +
+ +
+
+
+
+
+
+ + + +

+ {% if mode == 'update-processing' %} + Dispatch + {{ obj.getID() }} + {% elseif mode == 'update-assigning' %} + Rider Assignment + {{ obj.getID() }} + {% elseif mode == 'update-reassign-hub' %} + Re-assign Hub + {{ obj.getID() }} + {% elseif mode == 'update-reassign-rider' %} + Re-assign Rider + {{ obj.getID() }} + {% elseif mode == 'update-all' %} + Viewing + {{ obj.getID() }} + {% else %} + Incoming + {% endif %} +

+
+
+
+
+ + + {% if mode in ['update-assigning', 'update-processing', 'update-reassign-hub', 'update-reassign-rider', 'update-all', 'open_edit'] %} + + {% endif %} +
+ + {% if ftags.vehicle_dropdown %} +
+
+
+ + + +
+ +
+
+
+
+
+ + + +
+
+
+ {% else %} + + {% endif %} + {% if obj.getReferenceJO %} +
+
+
+ + + +
+
+
+ {% endif %} + +
+
+

+ Customer Details +

+
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+

+ Vehicle Details +

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+

+ Battery Details +

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+

+ Transaction Details +

+ + + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ +
+ + + + +
+ +
+
+ +
+ + + + +
+ +
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+ + + +
+
+
+ + +
+
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+ + + +
+
+
+ +
+
+
+
+

+ Location +

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + + +
+ + + + +
+
+
+
+
+
+
+
+

+ Invoice +

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + {% if ftags.invoice_edit %} + + + {% else %} + + {% endif %} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + + + + + + + + {% if not obj.getInvoice or (obj.getInvoice and obj.getInvoice.getItems|length == 0) %} + + + + {% else %} + {% for item in obj.getInvoice.getItems %} + + + + + + + {% endfor %} + {% endif %} + +
ItemQuantityUnit PriceAmount
+ No items to display. +
{{ item.getTitle }}{{ item.getQuantity|number_format }}{{ item.getPrice|number_format(2) }}{{ (item.getPrice * item.getQuantity)|number_format(2) }}
+
+
+ {% if ftags.invoice_edit %} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ {% endif %} +
+ + {% if mode in ['update-processing', 'update-reassign-hub'] %} +
+
+
+

+ Nearest Hubs +

+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + {% for hub in hubs %} + + + + + + + + + + {% endfor %} + +
HubBranchAvailable RidersJobs For AssignmentContact NumbersAction
+ No items to display. +
{{ hub.hub.getName }}{{ hub.hub.getBranch }}{{ hub.rider_count }}{{ hub.jo_count }}{{ hub.hub.getContactNumbers|replace({"\n": ', '}) }} + {% if hub.flag_rejected %} + + {% else %} + + {% endif %} +
+
+
+
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+ {% endif %} + + {% if mode in ['update-assigning', 'update-fulfillment', 'update-reassign-rider', 'update-all'] %} +
+ {% if obj.getHub %} +
+
+

+ Hub Details +

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + +
+
+
+ {% endif %} + +
+ + {% if mode in ['update-assigning', 'update-reassign-rider'] %} +
+
+

+ Rider Assignment +

+
+
+
+ + +
+ + + + + + + + + + + + + {% set avail_riders = obj.getHub.getAvailableRiders|default([]) %} + + + + + {% if obj.getHub %} + {% for rider in avail_riders %} + + + + + + + + + {% endfor %} + {% endif %} + +
First NameLast NameContact No.Plate NumberStatus
+ No riders available. +
+
+
{{ rider.getFirstName }}{{ rider.getLastName }}{{ rider.getContactNumber }}{{ rider.getPlateNumber }}
+
+
+
+
+ {% endif %} + + {% if mode in ['update-fulfillment', 'update-all'] %} + {% if obj.getRider %} +
+
+

+ Rider Details +

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+ {% endif %} + {% endif %} + {% endif %} + + {% if mode == 'update-all' %} +
+ +
+
+

+ Timeline +

+
+
+
+
+
+ {% for event in obj.getEvents %} +
+ + {{ event.getDateHappen|date("M j, Y") }} +
{{ event.getDateHappen|date("h:i:s a") }}
+
+
+ +
+
+ {{ event.getTypeName }} by {{ event.getUser.getFullName|default('Application') }} {% if event.getRider %} - Rider - {{ event.getRider.getFullName }}{% endif %} +
+
+ {% endfor %} +
+
+
+
+
+ {% endif %} + + {% if ftags.ticket_table %} +
+ +
+
+

+ Tickets +

+
+
+
+
+
+
+
+ +
+
+ {% endif %} + +
+
+
+
+
+ {% if mode != 'update-all' %} + + {% endif %} + {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} + Cancel Job Order + {% endif %} + {% if mode != 'create' %} + Back + {% endif %} +
+
+
+
+
+
+
+
+
+ + {% if mode in ['update-processing', 'update-reassign-hub'] %} + + + {% endif %} +{% endblock %} + +{% block scripts %} +{{ include('map/' ~ map_js_file) }} + + + + +{% endblock %} +