diff --git a/README.md b/README.md new file mode 100644 index 00000000..998e0dba --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +MQTT format for location updates: +`:` \ No newline at end of file diff --git a/composer.json b/composer.json index e118abfc..7219d070 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "predis/predis": "^1.1", "sensio/framework-extra-bundle": "^5.1", "setasign/fpdf": "^1.8", + "symfony/asset": "^4.0", "symfony/console": "^4.0", "symfony/debug": "^4.0", "symfony/filesystem": "^4.0", diff --git a/composer.lock b/composer.lock index b3630199..2aa7742d 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4873ae3fd18db755bc9bf395bbbfb141", + "content-hash": "b101ecfbc1f6f2270f0e8ad326035b7e", "packages": [ { "name": "catalyst/auth-bundle", @@ -2411,6 +2411,62 @@ ], "time": "2016-01-01T17:47:15+00:00" }, + { + "name": "symfony/asset", + "version": "v4.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "2c67c89d064bfb689ea6bc41217c87100bb94c17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/2c67c89d064bfb689ea6bc41217c87100bb94c17", + "reference": "2c67c89d064bfb689ea6bc41217c87100bb94c17", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "require-dev": { + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "suggest": { + "symfony/http-foundation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Asset Component", + "homepage": "https://symfony.com", + "time": "2020-01-04T13:00:46+00:00" + }, { "name": "symfony/cache", "version": "v4.3.1", @@ -5344,6 +5400,7 @@ "code", "zf2" ], + "abandoned": "laminas/laminas-code", "time": "2018-08-13T20:36:59+00:00" }, { @@ -5398,6 +5455,7 @@ "events", "zf2" ], + "abandoned": "laminas/laminas-eventmanager", "time": "2018-04-25T15:33:34+00:00" } ], diff --git a/config/packages/security.yaml b/config/packages/security.yaml index c8deb304..dc9aa78d 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -21,6 +21,11 @@ security: methods: [GET] security: false + tracker: + pattern: ^\/track\/ + methods: [GET] + security: false + api: pattern: ^\/api\/ security: false diff --git a/config/routes/job_order.yaml b/config/routes/job_order.yaml index 139b208f..bfd41df9 100644 --- a/config/routes/job_order.yaml +++ b/config/routes/job_order.yaml @@ -201,3 +201,8 @@ jo_ajax_popup: controller: App\Controller\JobOrderController::popupInfo methods: [GET] +jo_tracker: + path: /track/{id} + controller: App\Controller\JobOrderController::tracker + methods: [GET] + diff --git a/config/services.yaml b/config/services.yaml index e3a9c6f1..5364d19a 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -76,6 +76,7 @@ services: App\Service\MQTTClient: arguments: $redis_client: "@App\\Service\\RedisClientProvider" + $key: "mqtt_events" App\Service\APNSClient: arguments: @@ -87,7 +88,6 @@ services: $host: "%env(REDIS_CLIENT_HOST)%" $port: "%env(REDIS_CLIENT_PORT)%" $password: "%env(REDIS_CLIENT_PASSWORD)%" - $env_flag: "dev" App\Service\GeofenceTracker: arguments: @@ -205,3 +205,28 @@ services: App\Service\GISManagerInterface: "@App\\Service\\GISManager\\OpenStreet" #App\Service\GISManagerInterface: "@App\\Service\\GISManager\\Google" + App\EventListener\JobOrderActiveCacheListener: + arguments: + $jo_cache: "@App\\Service\\JobOrderCache" + $mqtt: "@App\\Service\\MQTTClient" + tags: + - name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'App\Entity\JobOrder' + - name: 'doctrine.orm.entity_listener' + event: 'postRemove' + entity: 'App\Entity\JobOrder' + - name: 'doctrine.orm.entity_listener' + event: 'postPersist' + entity: 'App\Entity\JobOrder' + + App\Service\JobOrderCache: + arguments: + $redis_prov: "@App\\Service\\RedisClientProvider" + $active_jo_key: "%env(LOCATION_JO_ACTIVE_KEY)%" + + App\Service\RiderCache: + arguments: + $redis_prov: "@App\\Service\\RedisClientProvider" + $loc_key: "%env(LOCATION_RIDER_ACTIVE_KEY)%" + $status_key: "%env(STATUS_RIDER_KEY)%" diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 051d2143..140966da 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -331,4 +331,31 @@ span.has-danger, .map-div-icon i.awesome { margin: 12px auto; font-size: 17px; +} +.map-info { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + padding: 1.5em; + width: 100%; +} + +.map-info > .m-portlet { + margin-bottom: 0; +} + +.map-info .m-portlet__body { + padding: 1.5rem; +} + +.map-info .rider-image { + width: 4.8rem; + border-radius: 50%; +} + +.map-info .m-badge { + border-radius: 0; +} \ No newline at end of file diff --git a/public/assets/js/dashboard_map.js b/public/assets/js/dashboard_map.js new file mode 100644 index 00000000..4691a11d --- /dev/null +++ b/public/assets/js/dashboard_map.js @@ -0,0 +1,171 @@ +class DashboardMap { + constructor(options, rider_markers, cust_markers) { + this.options = options; + this.rider_markers = rider_markers; + this.cust_markers = cust_markers; + + // layer groups + this.layer_groups = { + 'rider_available': L.layerGroup(), + 'rider_active_jo': L.layerGroup(), + 'customer': L.layerGroup() + }; + } + + initialize() { + // main map + this.map = L.map(this.options.div_id).setView( + [this.options.center_lat, this.options.center_lng], + this.options.zoom + ); + + // add tile layer + var streets = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', { + attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox', + maxZoom: 18, + id: 'mapbox/streets-v11', + accessToken: this.options.access_token + }).addTo(this.map); + + // layer groups + this.layer_groups.rider_available.addTo(this.map); + this.layer_groups.rider_active_jo.addTo(this.map); + this.layer_groups.customer.addTo(this.map); + + // base layer + var baseMaps = { + 'Streets': streets + }; + + if (this.options.display_overlay) { + // overlay layer + var overlayMaps = { + 'Available Riders' : this.layer_groups.rider_available, + 'JO Riders' : this.layer_groups.rider_active_jo, + 'Customers' : this.layer_groups.customer + } + + L.control.layers(baseMaps, overlayMaps).addTo(this.map); + } + + return this.map; + } + + putMarker(id, lat, lng, markers, icon, layer_group, popup_url) { + var my = this; + // existing marker + if (markers.hasOwnProperty(id)) { + markers[id].setLatLng(L.latLng(lat, lng)); + return; + } + + // new marker + markers[id] = L.marker( + [lat, lng], + { icon: icon } + ).addTo(layer_group); + + if (my.options.enable_popup) { + markers[id].bindPopup('Loading...'); + + // bind ajax for popup + markers[id].on('click', function(e) { + var popup = e.target.getPopup(); + var url = popup_url.replace('[id]', id); + console.log(url); + $.get(url).done(function(data) { + popup.setContent(data); + popup.update(); + }); + }); + } + } + + putCustomerMarker(id, lat, lng) { + this.putMarker( + id, + lat, + lng, + this.cust_markers, + this.options.icons.customer, + this.layer_groups.customer, + this.options.cust_popup_url + ); + } + + 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 + if (!markers.hasOwnProperty(id)) { + console.log('no such marker to remove'); + return; + } + + layer_group.removeLayer(markers[id]); + } + + putRiderAvailableMarker(id, lat, lng) { + this.putMarker( + id, + lat, + lng, + this.rider_markers, + this.options.icons.rider_available, + this.layer_groups.rider_available, + this.options.rider_popup_url + ); + } + + putRiderActiveJOMarker(id, lat, lng) { + this.putMarker( + id, + lat, + lng, + this.rider_markers, + this.options.icons.rider_active_jo, + this.layer_groups.rider_active_jo, + this.options.rider_popup_url + ); + } + + loadLocations(location_url) { + console.log(this.rider_markers); + var my = this; + $.ajax({ + url: location_url, + }).done(function(response) { + // clear all markers + my.layer_groups.rider_available.clearLayers(); + my.layer_groups.rider_active_jo.clearLayers(); + my.layer_groups.customer.clearLayers(); + + // get riders and job orders + var riders = response.riders; + var jos = response.jos; + + // job orders + $.each(jos, function(id, data) { + var lat = data.latitude; + var lng = data.longitude; + + my.putCustomerMarker(id, lat, lng); + }); + + // riders + $.each(riders, function(id, data) { + var lat = data.latitude; + var lng = data.longitude; + + if (data.has_jo) + my.putRiderActiveJOMarker(id, lat, lng); + else + my.putRiderAvailableMarker(id, lat, lng); + }); + + // console.log(rider_markers); + }); + } +} diff --git a/public/assets/js/map_mqtt.js b/public/assets/js/map_mqtt.js new file mode 100644 index 00000000..6eac1f65 --- /dev/null +++ b/public/assets/js/map_mqtt.js @@ -0,0 +1,114 @@ +class MapEventHandler { + constructor(options, dashmap) { + this.options = options; + this.dashmap = dashmap; + } + + connect(user_id, host, port) { + var d = new Date(); + var client_id = "dash-" + user_id + "-" + d.getMonth() + "-" + d.getDate() + "-" + d.getHours() + "-" + d.getMinutes() + "-" + d.getSeconds() + "-" + d.getMilliseconds(); + console.log(client_id); + + this.mqtt = new Paho.MQTT.Client(host, port, client_id); + var options = { + // useSSL: true, + timeout: 3, + invocationContext: this, + onSuccess: this.onConnect.bind(this), + }; + + this.mqtt.onMessageArrived = this.onMessage.bind(this); + + console.log('connecting to mqtt server...'); + this.mqtt.connect(options); + } + + onConnect(icontext) { + console.log('mqtt connected!'); + var my = icontext.invocationContext; + + // subscribe to rider locations + if (my.options.track_rider) { + console.log('subscribing to ' + my.options.channels.rider_location); + my.mqtt.subscribe(my.options.channels.rider_location); + } + + // subscribe to jo locations + if (my.options.track_jo) { + console.log('subscribing to ' + my.options.channels.jo_location); + my.mqtt.subscribe(my.options.channels.jo_location); + my.mqtt.subscribe(my.options.channels.jo_status); + } + } + + onMessage(msg) { + // console.log(msg); + console.log('received message'); + + var channel = msg.destinationName; + var chan_split = channel.split('/'); + var payload = msg.payloadString; + + // handle different channels + switch (chan_split[0]) { + case "rider": + this.handleRider(chan_split, payload); + break; + case "jo": + this.handleJobOrder(chan_split, payload); + break; + } + } + + handleRider(chan_split, payload) { + console.log("rider message"); + switch (chan_split[2]) { + case "location": + console.log("got location for rider " + chan_split[1] + " - " + payload); + var pl_split = payload.split(':'); + console.log(pl_split); + + // check for correct format + if (pl_split.length != 2) + break; + + var lat = parseFloat(pl_split[0]); + var lng = parseFloat(pl_split[1]); + + this.dashmap.putRiderAvailableMarker(chan_split[1], lat, lng); + break; + } + } + + handleJobOrder(chan_split, payload) { + console.log("jo message"); + var id = chan_split[1]; + switch (chan_split[2]) { + case "location": + // var my = this; + console.log("got location for jo " + id + " - " + payload); + var pl_split = payload.split(':'); + + // check for correct format + if (pl_split.length != 2) + break; + + var lat = parseFloat(pl_split[0]); + var lng = parseFloat(pl_split[1]); + + // move marker + console.log(lat + ' - ' + lng); + + this.dashmap.putCustomerMarker(id, lat, lng); + break; + case "status": + switch (payload) { + case 'cancel': + case 'fulfill': + case 'delete': + this.dashmap.removeCustomerMarker(id); + break; + } + } + } +} diff --git a/src/Command/CreateCustomerFromWarrantyCommand.php b/src/Command/CreateCustomerFromWarrantyCommand.php index e644fc06..91684217 100644 --- a/src/Command/CreateCustomerFromWarrantyCommand.php +++ b/src/Command/CreateCustomerFromWarrantyCommand.php @@ -313,17 +313,14 @@ class CreateCustomerFromWarrantyCommand extends Command protected function loadCustomers() { - error_log('starting query...'); // get all customers $customers = $this->em->getRepository(Customer::class)->findAll(); $cust_q = $this->em->createQuery('select c from App\Entity\Customer c'); $cust_iter = $q->iterate(); - error_log('looping through query...'); $this->cust_index = []; foreach ($cust_iter as $customer) { - error_log('here'); $mobile = trim($customer->getPhoneMobile()); if (!empty($mobile)) { diff --git a/src/Command/RefreshJobOrderCacheCommand.php b/src/Command/RefreshJobOrderCacheCommand.php new file mode 100644 index 00000000..cdf904d2 --- /dev/null +++ b/src/Command/RefreshJobOrderCacheCommand.php @@ -0,0 +1,68 @@ +em = $om; + $this->jo_cache = $jo_cache; + + parent::__construct(); + } + + protected function configure() + { + $this->setName('joborder:refresh_cache') + ->setDescription('Refresh active job order cache from database.') + ->setHelp('Refresh active job order cache from database.'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $date = new DateTime(); + $date->modify('-3 day'); + + $status_list = [ + JOStatus::PENDING, + JOStatus::RIDER_ASSIGN, + JOStatus::ASSIGNED, + JOStatus::IN_TRANSIT, + JOStatus::IN_PROGRESS, + ]; + + $qb = $this->em->getRepository(JobOrder::class) + ->createQueryBuilder('jo'); + $res = $qb->select('jo') + ->where('jo.status IN (:statuses)') + ->andWhere('jo.date_schedule >= :date') + ->setParameter('statuses', $status_list, Connection::PARAM_STR_ARRAY) + ->setParameter('date', $date) + ->getQuery() + ->execute(); + + // fulfill each + foreach ($res as $jo) + { + $this->jo_cache->addActiveJobOrder($jo); + } + } +} diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 3a3e5173..5ba473af 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -9,6 +9,8 @@ use Doctrine\ORM\EntityManagerInterface; use App\Service\RiderTracker; use App\Service\GISManagerInterface; +use App\Service\JobOrderCache; +use App\Service\RiderCache; use App\Entity\Rider; @@ -18,8 +20,11 @@ class HomeController extends Controller /** * @Menu(selected="home") */ - public function index(EntityManagerInterface $em, RiderTracker $rider_tracker, - GISManagerInterface $gis_manager) + public function index( + EntityManagerInterface $em, + RiderTracker $rider_tracker, + GISManagerInterface $gis_manager + ) { // get map $params['map_js_file'] = $gis_manager->getJSInitFile(); @@ -27,11 +32,40 @@ class HomeController extends Controller return $this->render('home.html.twig', $params); } - public function getRiderLocations(EntityManagerInterface $em, RiderTracker $rider_tracker) + public function getMapLocations(JobOrderCache $jo_cache) { - // TODO: get active riders from cache - // TODO: get active JOs from cache + $active_jos = $jo_cache->getAllActiveJobOrders(); + + // get active JOs from cache + } + + public function getRiderLocations(JobOrderCache $jo_cache, RiderCache $rider_cache, EntityManagerInterface $em, RiderTracker $rider_tracker) + { + // get active JOs from cache + $active_jos = $jo_cache->getAllActiveJobOrders(); + $riders = $rider_cache->getAllActiveRiders(); + + // TODO: optimize this + // get all riders and figure out if they have active jos + foreach ($riders as $rider_id => $rider_data) + { + $rider = $em->getRepository(Rider::class)->find($rider_id); + if ($rider == null) + { + unset($riders[$rider_id]); + continue; + } + + $jo = $rider->getActiveJobOrder(); + if ($jo == null) + $riders[$rider_id]['has_jo'] = false; + else + $riders[$rider_id]['has_jo'] = true; + } + + // get active riders from cache // get all riders + /* $riders = $em->getRepository(Rider::class)->findAll(); $locations = []; @@ -81,9 +115,11 @@ class HomeController extends Controller ]; } + */ return $this->json([ - 'riders' => $locations, + 'jos' => $active_jos, + 'riders' => $riders, ]); } diff --git a/src/Controller/HubController.php b/src/Controller/HubController.php index ffce16f6..e1d41525 100644 --- a/src/Controller/HubController.php +++ b/src/Controller/HubController.php @@ -166,7 +166,7 @@ class HubController extends Controller { $this->denyAccessUnlessGranted('hub.add', null, 'No access.'); - error_log($req->request->get('time_open')); + //error_log($req->request->get('time_open')); // create new object $em = $this->getDoctrine()->getManager(); @@ -314,6 +314,7 @@ class HubController extends Controller 'name' => $hub->getName(), 'branch' => $hub->getBranch(), 'cnum' => $hub->getContactNumbers(), + 'distance' => $hub_res['distance'], ]; } diff --git a/src/Controller/JobOrderController.php b/src/Controller/JobOrderController.php index 039ea37d..5d6127e3 100644 --- a/src/Controller/JobOrderController.php +++ b/src/Controller/JobOrderController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Ramcar\JOStatus; use App\Ramcar\InvoiceCriteria; +use App\Ramcar\CMBServiceType; use App\Entity\CustomerVehicle; use App\Entity\Promo; @@ -23,6 +24,9 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; +use Doctrine\ORM\EntityManagerInterface; +use App\Service\RiderTracker; + use Catalyst\MenuBundle\Annotation\Menu; class JobOrderController extends Controller @@ -922,4 +926,29 @@ class JobOrderController extends Controller return $this->render('job-order/popup.html.twig', [ 'jo' => $jo ]); } + + /** + * @ParamConverter("jo", class="App\Entity\JobOrder") + */ + public function tracker( + EntityManagerInterface $em, + RiderTracker $rider_tracker, + GISManagerInterface $gis_manager, + JobOrder $jo + ) + { + if ($jo === null) + return new Response('No job order data'); + + $rider = $jo->getRider(); + + // get map + $params['jo'] = $jo; + $params['rider'] = $rider; + $params['rider_pos'] = $rider_tracker->getRiderLocation($rider->getID()); + $params['service_type'] = CMBServiceType::getName($jo->getServiceType()); + $params['map_js_file'] = $gis_manager->getJSInitFile(); + + return $this->render('job-order/tracker.html.twig', $params); + } } diff --git a/src/Controller/RAPIController.php b/src/Controller/RAPIController.php index 841f9959..0097f202 100644 --- a/src/Controller/RAPIController.php +++ b/src/Controller/RAPIController.php @@ -31,6 +31,7 @@ use App\Service\InvoiceGeneratorInterface; use App\Service\MQTTClient; use App\Service\WarrantyHandler; use App\Service\RedisClientProvider; +use App\Service\RiderCache; use App\Entity\RiderSession; use App\Entity\Customer; @@ -213,7 +214,7 @@ class RAPIController extends Controller return $res->getReturnResponse(); } - public function login(Request $req, EncoderFactoryInterface $ef, RedisClientProvider $redis) + public function login(Request $req, EncoderFactoryInterface $ef, RedisClientProvider $redis, RiderCache $rcache) { $required_params = [ 'user', @@ -255,6 +256,11 @@ class RAPIController extends Controller $rider->setAvailable(true); + $rider_id = $rider->getID(); + // cache rider location (default to hub) + // TODO: figure out longitude / latitude default + $rcache->addActiveRider($rider_id, 0, 0); + // TODO: log rider logging in $em->flush(); @@ -293,7 +299,7 @@ class RAPIController extends Controller return $res->getReturnResponse(); } - public function logout(Request $req) + public function logout(Request $req, RiderCache $rcache) { $required_params = []; $em = $this->getDoctrine()->getManager(); @@ -305,6 +311,9 @@ class RAPIController extends Controller $rider = $this->session->getRider(); $rider->setAvailable(false); + // remove from cache + $rcache->removeActiveRider($rider->getID()); + // remove rider from session $this->session->setRider(null); @@ -936,7 +945,6 @@ class RAPIController extends Controller // check if new battery if (($jo->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW) || ($jo->getServiceType() != CMBServiceType::BATTERY_REPLACEMENT_NEW)) - return; // customer vehicle diff --git a/src/Controller/StaticContentController.php b/src/Controller/StaticContentController.php index 395736d4..44c77595 100644 --- a/src/Controller/StaticContentController.php +++ b/src/Controller/StaticContentController.php @@ -148,7 +148,6 @@ class StaticContentController extends Controller $result = $em->getRepository(StaticContent::class)->find($id); if ($result != null) { - error_log($id); $error_array['id'] = 'Duplicate ID exists.'; } @@ -226,7 +225,6 @@ class StaticContentController extends Controller $result = $em->getRepository(StaticContent::class)->find($id); if ($result != null) { - error_log($id); $error_array['id'] = 'Duplicate ID exists.'; } diff --git a/src/Controller/VehicleController.php b/src/Controller/VehicleController.php index db82b18b..2df6ec74 100644 --- a/src/Controller/VehicleController.php +++ b/src/Controller/VehicleController.php @@ -241,6 +241,9 @@ class VehicleController extends Controller if (empty($row)) throw $this->createNotFoundException('The item does not exist'); + // get current batteries of vehicle to be updated + $current_batteries = $row->getBatteries(); + // set and save values $row->setMake($req->request->get('make')) ->setModelYearFrom($req->request->get('model_year_from')) @@ -251,7 +254,6 @@ class VehicleController extends Controller else $row->setDisplayMobile(false); - // validate $errors = $validator->validate($row); @@ -272,6 +274,50 @@ class VehicleController extends Controller else $row->setManufacturer($manufacturer); + // custom validation for batteries + $batteries = $req->request->get('batteries'); + if (!empty($batteries)) + { + // need to check if a battery has been removed + if (count($current_batteries) > count($batteries)) + { + // battery/batteries have been removed + foreach ($current_batteries as $cbatt) + { + $cbatt_id = $cbatt->getID(); + if (in_array($cbatt_id, $batteries)) + { + // do nothing, move to next element + continue; + } + else + { + // cbatt_id has been deleted + $battery = $em->getRepository(Battery::class)->find($cbatt_id); + + if (!empty($battery)) + { + $battery->removeVehicle($row); + } + } + } + } + } + else + { + // no more battery compatible with vehicle + foreach ($current_batteries as $c_battery) + { + $cbatt_id = $c_battery->getID(); + $battery = $em->getRepository(Battery::class)->find($cbatt_id); + + if (!empty($battery)) + { + $battery->removeVehicle($row); + } + } + } + // check if any errors were found if (!empty($error_array)) { // return validation failure response diff --git a/src/Controller/WarrantyController.php b/src/Controller/WarrantyController.php index 40afec1a..ae6da43f 100644 --- a/src/Controller/WarrantyController.php +++ b/src/Controller/WarrantyController.php @@ -567,7 +567,7 @@ class WarrantyController extends Controller { // call service to check if warranty details is incomplete and then update warranty // using details from csv file - error_log('Updating warranty for ' . $warr->getID()); + // error_log('Updating warranty for ' . $warr->getID()); $wh->updateWarranty($warr, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase); } } @@ -582,7 +582,7 @@ class WarrantyController extends Controller continue; } - error_log('Creating warranty for serial ' . $serial . ' and plate number ' . $plate_number); + // error_log('Creating warranty for serial ' . $serial . ' and plate number ' . $plate_number); $wh->createWarranty($serial, $plate_number, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase, $warranty_class); } diff --git a/src/EventListener/JobOrderActiveCacheListener.php b/src/EventListener/JobOrderActiveCacheListener.php new file mode 100644 index 00000000..541ce158 --- /dev/null +++ b/src/EventListener/JobOrderActiveCacheListener.php @@ -0,0 +1,105 @@ +jo_cache = $jo_cache; + $this->mqtt = $mqtt; + } + + // when a new job order comes in + public function postPersist(JobOrder $jo, LifecycleEventArgs $args) + { + $status = $jo->getStatus(); + + switch ($status) + { + // active + case JOStatus::PENDING: + case JOStatus::RIDER_ASSIGN: + case JOStatus::ASSIGNED: + case JOStatus::IN_TRANSIT: + case JOStatus::IN_PROGRESS: + $this->processActiveJO($jo); + break; + // inactive + case JOStatus::CANCELLED: + $this->processInactiveJO($jo, 'cancel'); + break; + case JOStatus::FULFILLED: + $this->processInactiveJO($jo, 'fulfill'); + break; + } + } + + // when a job order is updated + public function postUpdate(JobOrder $jo, LifecycleEventArgs $args) + { + $status = $jo->getStatus(); + + switch ($status) + { + // active + case JOStatus::PENDING: + case JOStatus::RIDER_ASSIGN: + case JOStatus::ASSIGNED: + case JOStatus::IN_TRANSIT: + case JOStatus::IN_PROGRESS: + $this->processActiveJO($jo); + break; + // inactive + case JOStatus::CANCELLED: + $this->processInactiveJO($jo, 'cancel'); + break; + case JOStatus::FULFILLED: + $this->processInactiveJO($jo, 'fulfill'); + break; + } + } + + // when a job order is deleted + public function postRemove(JobOrder $jo, LifecycleEventArgs $args) + { + $this->processInactiveJO($jo, 'delete'); + } + + protected function processActiveJO($jo) + { + // save in cache + $this->jo_cache->addActiveJobOrder($jo); + + // publish to mqtt + $coords = $jo->getCoordinates(); + + // TODO: do we put the key in config? + $this->mqtt->publish( + 'jo/' . $jo->getID() . '/location', + $coords->getLatitude() . ':' . $coords->getLongitude() + ); + } + + protected function processInactiveJO($jo, $status = 'cancel') + { + // remove from redis cache + $this->jo_cache->removeActiveJobOrder($jo); + + // TODO: publich to mqtt + $this->mqtt->publish( + 'jo/' . $jo->getID() . '/status', + $status + ); + } +} + diff --git a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php index 72e196fe..c5e9c81c 100644 --- a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php +++ b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php @@ -594,7 +594,7 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface $diesel_price = self::REFUEL_FEE_DIESEL; $fuel = new InvoiceItem(); - error_log('fuel type - ' . $ftype); + //error_log('fuel type - ' . $ftype); switch ($ftype) { case FuelType::GAS: @@ -631,4 +631,77 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface $total['vat'] = $vat; } + public function processCriteria(InvoiceCriteria $criteria) + { + // initialize + $invoice = new Invoice(); + $total = [ + 'sell_price' => 0.0, + 'vat' => 0.0, + 'vat_ex_price' => 0.0, + 'ti_rate' => 0.0, + 'total_price' => 0.0, + 'discount' => 0.0, + ]; + + $stype = $criteria->getServiceType(); + $cv = $criteria->getCustomerVehicle(); + $has_coolant = $criteria->hasCoolant(); + // error_log($stype); + switch ($stype) + { + case ServiceType::JUMPSTART_TROUBLESHOOT: + $this->processJumpstart($total, $invoice); + break; + case ServiceType::JUMPSTART_WARRANTY: + $this->processJumpstartWarranty($total, $invoice); + + case ServiceType::BATTERY_REPLACEMENT_NEW: + $this->processEntries($total, $criteria, $invoice); + /* + $this->processBatteries($total, $criteria, $invoice); + $this->processTradeIns($total, $criteria, $invoice); + */ + $this->processDiscount($total, $criteria, $invoice); + break; + + case ServiceType::BATTERY_REPLACEMENT_WARRANTY: + $this->processWarranty($total, $criteria, $invoice); + break; + case ServiceType::POST_RECHARGED: + $this->processRecharge($total, $invoice); + break; + case ServiceType::POST_REPLACEMENT: + $this->processReplacement($total, $invoice); + break; + case ServiceType::TIRE_REPAIR: + $this->processTireRepair($total, $invoice, $cv); + // $this->processOtherServices($total, $invoice, $stype); + break; + case ServiceType::OVERHEAT_ASSISTANCE: + $this->processOverheat($total, $invoice, $cv, $has_coolant); + break; + case ServiceType::EMERGENCY_REFUEL: + //error_log('processing refuel'); + $ftype = $criteria->getCustomerVehicle()->getFuelType(); + $this->processRefuel($total, $invoice, $cv); + break; + } + + // TODO: check if any promo is applied + // apply discounts + $promos = $criteria->getPromos(); + + $invoice->setTotalPrice($total['total_price']) + ->setVATExclusivePrice($total['vat_ex_price']) + ->setVAT($total['vat']) + ->setDiscount($total['discount']) + ->setTradeIn($total['ti_rate']); + + + // dump + //Debug::dump($invoice, 1); + + return $invoice; + } } diff --git a/src/Service/JobOrderCache.php b/src/Service/JobOrderCache.php new file mode 100644 index 00000000..488db29d --- /dev/null +++ b/src/Service/JobOrderCache.php @@ -0,0 +1,66 @@ +redis = $redis_prov->getRedisClient(); + $this->active_jo_key = $active_jo_key; + } + + public function addActiveJobOrder(JobOrder $jo) + { + $coords = $jo->getCoordinates(); + + $this->redis->geoadd( + $this->active_jo_key, + $coords->getLongitude(), + $coords->getLatitude(), + $jo->getID() + ); + } + + public function getAllActiveJobOrders() + { + $all_jo = $this->redis->georadius( + $this->active_jo_key, + 0, + 0, + 41000, + 'km', + ['WITHCOORD' => true] + ); + + $jo_locs = []; + foreach ($all_jo as $jo_data) + { + $id = $jo_data[0]; + $lng = $jo_data[1][0]; + $lat = $jo_data[1][1]; + + $jo_locs[$id] = [ + 'longitude' => $lng, + 'latitude' => $lat, + ]; + } + + // error_log(print_r($all_jo, true)); + return $jo_locs; + } + + public function removeActiveJobOrder(JobOrder $jo) + { + $this->redis->zrem( + $this->active_jo_key, + $jo->getID() + ); + } +} diff --git a/src/Service/JobOrderHandler/CMBJobOrderHandler.php b/src/Service/JobOrderHandler/CMBJobOrderHandler.php index 4803e1ec..50c504fc 100644 --- a/src/Service/JobOrderHandler/CMBJobOrderHandler.php +++ b/src/Service/JobOrderHandler/CMBJobOrderHandler.php @@ -164,7 +164,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $row['delivery_address'] = $orow->getDeliveryAddress(); $row['date_schedule'] = $orow->getDateSchedule()->format("d M Y g:i A"); $row['type'] = $orow->isAdvanceOrder() ? 'Advanced Order' : 'Immediate'; - $row['service_type'] = $service_types[$orow->getServiceType()]; + $row['service_type'] = $service_types[$orow->getServiceType()] ?? 'Unknown'; $row['status'] = $statuses[$orow->getStatus()]; $row['flag_advance'] = $orow->isAdvanceOrder(); $row['plate_number'] = $orow->getCustomerVehicle()->getPlateNumber(); diff --git a/src/Service/MQTTClient.php b/src/Service/MQTTClient.php index aa0932a3..c9c7a4f2 100644 --- a/src/Service/MQTTClient.php +++ b/src/Service/MQTTClient.php @@ -9,14 +9,15 @@ class MQTTClient { const PREFIX = 'motolite.control.'; const RIDER_PREFIX = 'motorider_'; - const REDIS_KEY = 'events'; // protected $mclient; protected $redis; + protected $key; - public function __construct(RedisClientProvider $redis_client) + public function __construct(RedisClientProvider $redis_client, $key) { $this->redis = $redis_client->getRedisClient(); + $this->key = $key; } public function __destruct() @@ -29,7 +30,7 @@ class MQTTClient // $this->mclient->publish($channel, $message); $data = $channel . '|' . $message; - $this->redis->lpush(self::REDIS_KEY, $data); + $this->redis->lpush($this->key, $data); } public function sendEvent(JobOrder $job_order, $payload) diff --git a/src/Service/MapTools.php b/src/Service/MapTools.php index f02618ba..6aa5848c 100644 --- a/src/Service/MapTools.php +++ b/src/Service/MapTools.php @@ -84,10 +84,23 @@ class MapTools { //error_log($row[0]->getName() . ' - ' . $row['dist']); $hubs[] = $row[0]; + + // get coordinates of hub + $hub_coordinates = $row[0]->getCoordinates(); + + $cust_lat = $point->getLatitude(); + $cust_lng = $point->getLongitude(); + + $hub_lat = $hub_coordinates->getLatitude(); + $hub_lng = $hub_coordinates->getLongitude(); + + // get distance in kilometers from customer point to hub point + $dist = $this->distance($cust_lat, $cust_lng, $hub_lat, $hub_lng); + $final_data[] = [ 'hub' => $row[0], 'db_distance' => $row['dist'], - 'distance' => 0, + 'distance' => $dist, 'duration' => 0, ]; } @@ -135,4 +148,18 @@ class MapTools return $final_data; */ } + + protected function distance($lat1, $lon1, $lat2, $lon2) + { + if (($lat1 == $lat2) && ($lon1 == $lon2)) + return 0; + + $theta = $lon1 - $lon2; + $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); + $dist = acos($dist); + $dist = rad2deg($dist); + $miles = $dist * 60 * 1.1515; + + return round(($miles * 1.609344), 1); + } } diff --git a/src/Service/RedisClientProvider.php b/src/Service/RedisClientProvider.php index 04ac1f80..80dd06fa 100644 --- a/src/Service/RedisClientProvider.php +++ b/src/Service/RedisClientProvider.php @@ -11,34 +11,46 @@ class RedisClientProvider protected $host; protected $port; protected $password; - protected $env_flag; - public function __construct($scheme, $host, $port, $password, $env_flag) + public function __construct($scheme, $host, $port, $password) { $this->scheme = $scheme; $this->host = $host; $this->port = $port; $this->password = $password; - $this->env_flag = $env_flag; + $this->redis = null; + + $this->connect(); + } + + protected function connect() + { + // already connected + if ($this->redis != null) + return $this->redis; + + // if password is specified attempt connection + if (strlen($this->password) > 0) + { + $this->redis = new PredisClient([ + "scheme" => $this->scheme, + "host" => $this->host, + "port" => $this->port, + "password" => $this->password]); + + return $this->redis; + } + + $this->redis = new PredisClient([ + "scheme" => $this->scheme, + "host" => $this->host, + "port" => $this->port]); + + return $this->redis; } public function getRedisClient() { - if ($this->env_flag == 'dev') - { - $this->redis = new PredisClient([ - "scheme"=>$this->scheme, - "host"=>$this->host, - "port"=>$this->port]); - } - else - { - $this->redis = new PredisClient([ - "scheme"=>$this->scheme, - "host"=>$this->host, - "port"=>$this->port, - "password"=>$this->password]); - } return $this->redis; } } diff --git a/src/Service/RiderCache.php b/src/Service/RiderCache.php new file mode 100644 index 00000000..cdd18864 --- /dev/null +++ b/src/Service/RiderCache.php @@ -0,0 +1,76 @@ +redis = $redis_prov->getRedisClient(); + $this->loc_key = $loc_key; + $this->status_key = $status_key; + } + + public function addActiveRider($id, $lat, $lng) + { + $this->redis->geoadd( + $this->loc_key, + $lng, + $lat, + $id + ); + } + + public function getAllActiveRiders() + { + $all_riders = $this->redis->georadius( + $this->loc_key, + 0, + 0, + 41000, + 'km', + ['WITHCOORD' => true] + ); + + $locs = []; + foreach ($all_riders as $data) + { + $id = $data[0]; + $lng = $data[1][0]; + $lat = $data[1][1]; + + $locs[$id] = [ + 'longitude' => $lng, + 'latitude' => $lat, + ]; + } + + // error_log(print_r($all_riders, true)); + return $locs; + } + + public function removeActiveRider($id) + { + $this->redis->zrem( + $this->loc_key, + $id + ); + } + + public function incJobOrderCount($id, $status) + { + $this->redis->hincrby($this->status_key, $id, 1); + } + + public function decJobOrderCount($id, $status) + { + $this->redis->hincrby($this->status_key, $id, -1); + } +} diff --git a/src/Service/WarrantyHandler.php b/src/Service/WarrantyHandler.php index 641658d7..281464bf 100644 --- a/src/Service/WarrantyHandler.php +++ b/src/Service/WarrantyHandler.php @@ -93,7 +93,7 @@ class WarrantyHandler public function updateCustomerVehicle($serial, $batteries, $plate_number, $date_expire) { // find customer vehicle using plate number - error_log('Finding customer vehicle with plate number ' . $plate_number); + // error_log('Finding customer vehicle with plate number ' . $plate_number); $cv_q = $this->em->createQuery('select count(cv) from App\Entity\CustomerVehicle cv where cv.plate_number = :plate_number') ->setParameter('plate_number', $plate_number); $cv_result = $cv_q->getSingleScalarResult(); @@ -288,7 +288,7 @@ class WarrantyHandler if (empty($warranty_class)) { - error_log('Warranty class is empty for warranty id ' . $warr->getID()); + //error_log('Warranty class is empty for warranty id ' . $warr->getID()); return null; } @@ -301,12 +301,12 @@ class WarrantyHandler { if ($batt_model == null) { - error_log('Battery model is null for warranty id ' . $warr->getID()); + //error_log('Battery model is null for warranty id ' . $warr->getID()); return null; } if ($batt_size == null) { - error_log('Battery size is null for warranty id ' . $warr->getID()); + //error_log('Battery size is null for warranty id ' . $warr->getID()); return null; } diff --git a/symfony.lock b/symfony.lock index 88d10d33..576e49fd 100644 --- a/symfony.lock +++ b/symfony.lock @@ -152,6 +152,9 @@ "setasign/fpdf": { "version": "1.8.1" }, + "symfony/asset": { + "version": "v4.4.3" + }, "symfony/cache": { "version": "v4.0.2" }, diff --git a/templates/base_minimal.html.twig b/templates/base_minimal.html.twig new file mode 100644 index 00000000..8a51bcc6 --- /dev/null +++ b/templates/base_minimal.html.twig @@ -0,0 +1,70 @@ +{% import 'menu.html.twig' as menu %} + + + + + + + {% block title %}{% trans %}block_title{% endtrans %}{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + + + {% block stylesheets %} + + {% endblock %} + + + + + + + {% block body %}{% endblock %} + + + + + + + + + + + + + + + + {% block scripts %}{% endblock %} + + + diff --git a/templates/home.html.twig b/templates/home.html.twig index f55fe863..975b0ea5 100644 --- a/templates/home.html.twig +++ b/templates/home.html.twig @@ -13,111 +13,78 @@ {% block scripts %} + + +{{ include('map/' ~ map_js_file) }} - -{{ include('map/' ~ map_js_file) }} - - - {% endblock %} diff --git a/templates/job-order/cmb.form.onestep.html.twig b/templates/job-order/cmb.form.onestep.html.twig index 5fff6343..8e4ac45d 100644 --- a/templates/job-order/cmb.form.onestep.html.twig +++ b/templates/job-order/cmb.form.onestep.html.twig @@ -381,18 +381,21 @@ Hub Branch Contact Numbers + Distance in KM Action - {% if mode in ['onestep-edit'] %} + @@ -582,6 +585,9 @@
+ {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} + Cancel Job Order + {% endif %} Back
@@ -626,7 +632,7 @@ $(function() { }); var icon_hub = L.divIcon({ className: 'map-div-icon', - html: "
", + html: "
", iconSize: [39, 42], iconAnchor: [15, 42] }); @@ -659,6 +665,7 @@ $(function() { hub_table += '' + hub['name'] + ''; hub_table += '' + hub['branch'] + ''; hub_table += '' + hub['cnum'] + ''; + hub_table += '' + hub['distance'] + ''; hub_table += ''; hub_table += ''; } @@ -670,13 +677,30 @@ $(function() { {% if mode in ['onestep-edit'] %} // get nearest hubs ajax + var hub_table = ''; $.getJSON("{{ url('hub_nearest') }}?lat=" + lat + "&long=" + lng, function(data) { var hubs = data['hubs']; for (i in hubs) { var hub = hubs[i]; var hub_marker = L.marker([hub['lat'], hub['long']], { icon: icon_hub }); hubLayerGroup.addLayer(hub_marker); + + if (selected_hub == hub['id']) { + hub_table += ''; + } + else { + hub_table += ''; + } + hub_table += '' + hub['name'] + ''; + hub_table += '' + hub['branch'] + ''; + hub_table += '' + hub['cnum'] + ''; + hub_table += '' + hub['distance'] + ''; + hub_table += ''; + hub_table += ''; + } + + $('#nearest_hubs').html(hub_table); }); {% endif %} @@ -743,14 +767,6 @@ $(function() { }); $(function() { - {% if mode in ['onestep-edit'] %} - selected_hub = '{{ obj.getHub ? obj.getHub.getID: "" }}'; - $('#hub-field').val(selected_hub); - {% endif %} - {% if mode in ['onestep'] %} - selected_hub = ''; - {% endif %} - $('#hubs-table').on('click', 'tr', function() { var id = $(this).data('id'); @@ -806,13 +822,6 @@ $(function() { }); $(function() { - {% if mode in ['onestep-edit'] %} - selected_rider = '{{ obj.getRider ? obj.getRider.getID: "" }}'; - $('#rider-field').val(selected_rider); - {% endif %} - {% if mode in ['onestep'] %} - selected_rider = ''; - {% endif %} $('#rider-table').on('click', 'tr', function() { var id = $(this).data('id'); @@ -830,12 +839,30 @@ $(function() { {% if mode in ['onestep-edit'] %} var lat = {{ obj.getCoordinates.getLatitude }}; var lng = {{ obj.getCoordinates.getLongitude }}; - var hub = {{ obj.getHub.getID }}; - var rider = {{ obj.getRider.getID }}; + + selected_hub = '{{ obj.getHub ? obj.getHub.getID: "" }}'; + $('#hub-field').val(selected_hub); + + selected_rider = '{{ obj.getRider ? obj.getRider.getID: "" }}'; + $('#rider-field').val(selected_rider); selectPoint(lat, lng); - // TODO: find a way to highlight the set hub + // need to put selected rider on map. selected_hub is already on map because of selectPoint + riderLayerGroup.clearLayers(); + + $.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) { + var riders = data['riders']; + for (i in riders) { + var rider = riders[i]; + if (selected_rider == rider['id']) { + var rider_lat = rider['location'][0]; + var rider_lng = rider['location'][1]; + var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available }); + riderLayerGroup.addLayer(rider_marker); + } + } + }); {% endif %} {% if mode in ['update-processing', 'update-reassign-hub'] %} diff --git a/templates/job-order/form.onestep.html.twig b/templates/job-order/form.onestep.html.twig index aee7ca87..8e4ac45d 100644 --- a/templates/job-order/form.onestep.html.twig +++ b/templates/job-order/form.onestep.html.twig @@ -36,7 +36,9 @@
- +
@@ -326,7 +328,9 @@
- +
@@ -338,7 +342,9 @@
- + @@ -363,7 +369,9 @@
- +
@@ -372,25 +380,22 @@ Hub Branch - Contact Numbers + Distance in KM Action - {% if mode in ['onestep-edit'] %} +
@@ -405,7 +410,9 @@
- +
@@ -578,6 +585,9 @@
+ {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} + Cancel Job Order + {% endif %} Back
@@ -622,7 +632,7 @@ $(function() { }); var icon_hub = L.divIcon({ className: 'map-div-icon', - html: "
", + html: "
", iconSize: [39, 42], iconAnchor: [15, 42] }); @@ -655,24 +665,42 @@ $(function() { hub_table += '' + hub['name'] + ''; hub_table += '' + hub['branch'] + ''; hub_table += '' + hub['cnum'] + ''; + hub_table += '' + hub['distance'] + ''; hub_table += ''; hub_table += ''; } $('#nearest_hubs').html(hub_table); - + }); {% endif %} {% if mode in ['onestep-edit'] %} // get nearest hubs ajax + var hub_table = ''; $.getJSON("{{ url('hub_nearest') }}?lat=" + lat + "&long=" + lng, function(data) { var hubs = data['hubs']; for (i in hubs) { var hub = hubs[i]; var hub_marker = L.marker([hub['lat'], hub['long']], { icon: icon_hub }); hubLayerGroup.addLayer(hub_marker); + + if (selected_hub == hub['id']) { + hub_table += ''; + } + else { + hub_table += ''; + } + hub_table += '' + hub['name'] + ''; + hub_table += '' + hub['branch'] + ''; + hub_table += '' + hub['cnum'] + ''; + hub_table += '' + hub['distance'] + ''; + hub_table += ''; + hub_table += ''; + } + + $('#nearest_hubs').html(hub_table); }); {% endif %} @@ -739,13 +767,6 @@ $(function() { }); $(function() { - {% if mode in ['onestep-edit'] %} - selected_hub = '{{ obj.getHub ? obj.getHub.getID: "" }}'; - $('#hub-field').val(selected_hub); - {% endif %} - {% if mode in ['onestep'] %} - selected_hub = ''; - {% endif %} $('#hubs-table').on('click', 'tr', function() { var id = $(this).data('id'); @@ -801,13 +822,6 @@ $(function() { }); $(function() { - {% if mode in ['onestep-edit'] %} - selected_rider = '{{ obj.getRider ? obj.getRider.getID: "" }}'; - $('#rider-field').val(selected_rider); - {% endif %} - {% if mode in ['onestep'] %} - selected_rider = ''; - {% endif %} $('#rider-table').on('click', 'tr', function() { var id = $(this).data('id'); @@ -826,9 +840,29 @@ $(function() { var lat = {{ obj.getCoordinates.getLatitude }}; var lng = {{ obj.getCoordinates.getLongitude }}; + selected_hub = '{{ obj.getHub ? obj.getHub.getID: "" }}'; + $('#hub-field').val(selected_hub); + + selected_rider = '{{ obj.getRider ? obj.getRider.getID: "" }}'; + $('#rider-field').val(selected_rider); + selectPoint(lat, lng); - // TODO: find a way to highlight the set hub + // need to put selected rider on map. selected_hub is already on map because of selectPoint + riderLayerGroup.clearLayers(); + + $.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) { + var riders = data['riders']; + for (i in riders) { + var rider = riders[i]; + if (selected_rider == rider['id']) { + var rider_lat = rider['location'][0]; + var rider_lng = rider['location'][1]; + var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available }); + riderLayerGroup.addLayer(rider_marker); + } + } + }); {% endif %} {% if mode in ['update-processing', 'update-reassign-hub'] %} diff --git a/templates/job-order/popup.html.twig b/templates/job-order/popup.html.twig index 509c7296..d4e67d48 100644 --- a/templates/job-order/popup.html.twig +++ b/templates/job-order/popup.html.twig @@ -2,7 +2,12 @@ {% set cv = jo.getCustomerVehicle %} {{ cust.getNameDisplay }}
{{ cv.getPlateNumber }}
-Job Order #{{ jo.getID }}
+Job Order #{{ jo.getID }}
{{ jo.getServiceTypeName }}
{{ jo.getStatusText }} - +{% if jo.getRider != null %} +

+{% set rider = jo.getRider %} +{{ rider.getFullName }}
+{{ rider.getPlateNumber }} +{% endif %} diff --git a/templates/job-order/tracker.html.twig b/templates/job-order/tracker.html.twig new file mode 100644 index 00000000..661bcc64 --- /dev/null +++ b/templates/job-order/tracker.html.twig @@ -0,0 +1,118 @@ +{% extends 'base_minimal.html.twig' %} + +{% block body %} +
+
+
+
+
+
+ +
+
Order #{{ jo.getID }}
+
{{ rider.getFullName }}
+
{{ service_type }}
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + + + +{{ include('map/' ~ map_js_file) }} + +{% endblock %} diff --git a/templates/map/initOpenStreetMap.js b/templates/map/initOpenStreetMap.js index 358b91d4..8eab44bd 100644 --- a/templates/map/initOpenStreetMap.js +++ b/templates/map/initOpenStreetMap.js @@ -2,142 +2,3 @@ integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""> - - diff --git a/templates/rider/popup.html.twig b/templates/rider/popup.html.twig index 8807d6c7..13b201d0 100644 --- a/templates/rider/popup.html.twig +++ b/templates/rider/popup.html.twig @@ -4,7 +4,7 @@ {% set cust = jo.getCustomer %} {% set cv = jo.getCustomerVehicle %}
-Job Order #{{ jo.getID }}
+Job Order #{{ jo.getID }}
{{ jo.getServiceTypeName }}
{{ jo.getStatusText }}

{{ cust.getNameDisplay }}
diff --git a/templates/vehicle/form.html.twig b/templates/vehicle/form.html.twig index bd6fa482..4669d1c2 100644 --- a/templates/vehicle/form.html.twig +++ b/templates/vehicle/form.html.twig @@ -135,13 +135,23 @@ $(function() { $("#row-form").submit(function(e) { var form = $(this); + var formdata = form.serialize(); e.preventDefault(); + // add battery data + bdata = ''; + $.each(battRows, function(index, battery) { + bdata += "&batteries%5B%5D=" + battery.id; + }); + + // append to form data + formdata += bdata; + $.ajax({ method: "POST", url: form.prop('action'), - data: form.serialize() + data: formdata }).done(function(response) { // remove all error classes removeErrors(); @@ -192,6 +202,7 @@ $(function() { }); var battRows = []; + var batteryIds = []; {% for batt in obj.getBatteries %} trow = { @@ -202,8 +213,30 @@ $(function() { }; battRows.push(trow); + batteryIds.push({{ batt.getID }}); {% endfor %} + // remove battery from table + $(document).on('click', '.btn-delete', function(e) { + var btn = $(this); + var id = $(this).data('id'); + + $.each(battRows, function(index, battery) { + if (battery.id == id) { + battRows.splice(index, 1); + return false; + } + }); + + // remove from battery ids + batteryIds.splice(batteryIds.indexOf(id), 1); + + // reload table + battTable.row(btn.parents('tr')).remove(); + battTable.originalDataSet = battRows; + battTable.reload(); + }); + // battery data table var battOptions = { data: { @@ -235,7 +268,17 @@ $(function() { { field: 'sell_price', title: 'Price' - } + }, + { + field: 'Actions', + width: 70, + title: 'Actions', + sortable: false, + overflow: 'visible', + template: function (row, index, datatable) { + return ''; + }, + } ], pagination: false }; diff --git a/translations/messages.en.yaml b/translations/messages.en.yaml index 04ebdc42..992f8de9 100644 --- a/translations/messages.en.yaml +++ b/translations/messages.en.yaml @@ -26,4 +26,4 @@ default_lat: 14.6091 default_long: 121.0223 #default_lat: 3.084216 #default_long: 101.6129996 -default_region: my +default_region: ph diff --git a/utils/mqtt_rider/rider_location_cache.py b/utils/mqtt_rider/rider_location_cache.py new file mode 100644 index 00000000..b07d4acc --- /dev/null +++ b/utils/mqtt_rider/rider_location_cache.py @@ -0,0 +1,41 @@ +import paho.mqtt.client as mqtt +import ssl +import redis +import time +import signal +import sys +import os +import json + +class RiderLocationCache(object): + + def run(self, client): + print "running loop..." + client.loop_forever() + +# TODO: fix this and put these guys back under the class +def init_subscriptions(client): + print "subscribing to rider/+/location" + client.subscribe('rider/+/location') + +def on_connect(client, userdata, flags, rc): + init_subscriptions(client) + #print("Connected with result code "+str(rc)) + # client.subscribe("$SYS/#") + +def on_publish(client, userdata, mid): + pass + +def on_message(client, userdata, message): + redis_conn = userdata['redis'] + + topic_split = message.topic.split('/') + if topic_split[0] != 'rider': + return; + payload_split = message.payload.split(':') + + rider_long = str(payload_split[1]) + rider_lat = str(payload_split[0]) + + # set the location + redis_conn.geoadd('loc_rider_active', rider_long, rider_lat, topic_split[1]) diff --git a/utils/mqtt_rider/rider_location_cache.pyc b/utils/mqtt_rider/rider_location_cache.pyc new file mode 100644 index 00000000..e8968436 Binary files /dev/null and b/utils/mqtt_rider/rider_location_cache.pyc differ diff --git a/utils/mqtt_rider/riderloc.py b/utils/mqtt_rider/riderloc.py new file mode 100644 index 00000000..2b810cca --- /dev/null +++ b/utils/mqtt_rider/riderloc.py @@ -0,0 +1,26 @@ +import paho.mqtt.client as mqtt +import rider_location_cache as rlc +import ssl +import redis +import logging + +redis_client = redis.StrictRedis(host='localhost', port=6379, db=0) +userdata = {'redis': redis_client} + +client = mqtt.Client("", True, userdata) +client.on_connect = rlc.on_connect +# client.on_publish = on_publish +client.on_message = rlc.on_message + +#client.tls_set( +# "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE, +# tls_version=ssl.PROTOCOL_TLSv1) +#client.tls_set( +# "/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE, +# tls_version=ssl.PROTOCOL_TLSv1) +#client.connect("resqaws.jankstudio.com", 8883, 60) +client.connect("localhost", 1883, 60) + + +rider_location = rlc.RiderLocationCache() +rider_location.run(client) diff --git a/utils/mqtt_rider/riderloc.service b/utils/mqtt_rider/riderloc.service new file mode 100644 index 00000000..f3d43264 --- /dev/null +++ b/utils/mqtt_rider/riderloc.service @@ -0,0 +1,12 @@ +[Unit] +Description=Rider Location Cache Service +After=mosquitto.service redis.service + +[Service] +Type=simple +ExecStart=/usr/bin/python /root/www/resq/utils/rider_location_cache/riderloc.py +StandardInput=tty-force +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/utils/mqtt_sender/mqtt_sender.py b/utils/mqtt_sender/mqtt_sender.py index 54349755..f610b3db 100644 --- a/utils/mqtt_sender/mqtt_sender.py +++ b/utils/mqtt_sender/mqtt_sender.py @@ -1,7 +1,6 @@ import paho.mqtt.client as mqtt +import yaml import ssl -from threading import Thread -from daemonize import Daemonize import redis import time import signal @@ -10,29 +9,23 @@ import os import logging - -def sigint_handler(signal, frame): - #logging.warning('Interrupted') - sys.exit(0) - os._exit(0) - +# TODO: yaml configuration file for redis and mqtt settings def on_connect(client, userdata, flags, rc): - #logging.info("Connected with result code "+str(rc)) - client.subscribe("$SYS/#") - + logging.info("Connected with result code "+str(rc)) + #client.subscribe("$SYS/#") def on_publish(client, userdata, mid): pass -def getRedis(i, client, logger): +def redis_listen(client, logger): logger.info("Listening in redis events") r = redis.StrictRedis(host='localhost', port=6379, db=0) while 1: time.sleep(0) - data = r.brpop("events", 10) + data = r.brpop("mqtt_events", 10) if data: info = data[1].split('|') logger.info("Channel: " + info[0] + " message: " + info[1]) @@ -40,9 +33,6 @@ def getRedis(i, client, logger): -def sigint_handler(signal, frame): - sys.exit(0) - def get_logger(): logger = logging.getLogger("mqtt_logger") logger.setLevel(logging.INFO) @@ -64,25 +54,14 @@ def main(): client.on_connect = on_connect client.on_publish = on_publish - client.tls_set( - "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE, - tls_version=ssl.PROTOCOL_TLSv1) + # configure mqtt broker to accept localhost + client.connect("localhost", 1883, 60) - client.connect("resqaws.jankstudio.com", 8883, 60) + client.loop_start() + redis_listen(client, logger) + client.loop_end() - logger.info("Starting redis thread") - t = Thread(target=getRedis, args=(1, client, logger)) - - t.start() - - signal.signal(signal.SIGINT, sigint_handler) - client.loop_forever() + #client.loop_forever() -#logging.basicConfig(filename='/tmp/mqtt_sender.log', level=logging.INFO) -#logging.info('Started mqtt_sender') - -#pid = "/tmp/mqtt_sender.pid" -#daemon = Daemonize(app="mqtt_sender", pid=pid, action=main) -#daemon.start() main() diff --git a/utils/rider_location_cache/riderloc.py b/utils/rider_location_cache/riderloc.py index 26a1a92f..ca7776b1 100644 --- a/utils/rider_location_cache/riderloc.py +++ b/utils/rider_location_cache/riderloc.py @@ -12,9 +12,9 @@ client.on_message = rlc.on_message #client.tls_set( # "/etc/letsencrypt/live/resqaws.jankstudio.com/fullchain.pem", cert_reqs=ssl.CERT_NONE, # tls_version=ssl.PROTOCOL_TLSv1) -client.tls_set( - "/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE, - tls_version=ssl.PROTOCOL_TLSv1) +#client.tls_set( +# "/root/aws_ssl_keys/fullchain.pem", cert_reqs=ssl.CERT_NONE, +# tls_version=ssl.PROTOCOL_TLSv1) #client.connect("resqaws.jankstudio.com", 8883, 60) client.connect("localhost", 8883, 60)