diff --git a/config/routes/notification.yaml b/config/routes/notification.yaml new file mode 100644 index 00000000..f3a15104 --- /dev/null +++ b/config/routes/notification.yaml @@ -0,0 +1,9 @@ +notification_ajax_list: + path: /ajax/notifications + controller: App\Controller\NotificationController::ajaxList + methods: [GET] + +notification_ajax_update: + path: /ajax/notifications + controller: App\Controller\NotificationController::ajaxUpdate + methods: [POST] diff --git a/config/services.yaml b/config/services.yaml index 3d9fd7a3..7d585e08 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -221,6 +221,12 @@ services: event: 'postPersist' entity: 'App\Entity\JobOrder' + App\Service\NotificationManager: + arguments: + $redis_prov: "@App\\Service\\RedisClientProvider" + $redis_mqtt_key: "mqtt_events" + $em: "@doctrine.orm.entity_manager" + App\Service\JobOrderCache: arguments: $redis_prov: "@App\\Service\\RedisClientProvider" diff --git a/public/assets/js/dashboard_map.js b/public/assets/js/dashboard_map.js index 5d753cab..2df2d69f 100644 --- a/public/assets/js/dashboard_map.js +++ b/public/assets/js/dashboard_map.js @@ -231,30 +231,38 @@ class DashboardMap { ); } - putRiderAvailableMarker(id, lat, lng, name) { - this.putMarkerWithLabel( - id, - lat, - lng, - this.rider_markers, - this.options.icons.rider_available, - this.layer_groups.rider_available, - this.options.rider_popup_url, - name - ); + putRiderAvailableMarker(id, lat, lng) { + var my = this; + + my.getRiderName(id, function(name) { + my.putMarkerWithLabel( + id, + lat, + lng, + my.rider_markers, + my.options.icons.rider_available, + my.layer_groups.rider_available, + my.options.rider_popup_url, + name + ); + }); } - putRiderActiveJOMarker(id, lat, lng, name) { - this.putMarkerWithLabel( - id, - lat, - lng, - this.rider_markers, - this.options.icons.rider_active_jo, - this.layer_groups.rider_active_jo, - this.options.rider_popup_url, - name - ); + putRiderActiveJOMarker(id, lat, lng) { + var my = this; + + my.getRiderName(id, function(name) { + my.putMarkerWithLabel( + id, + lat, + lng, + my.rider_markers, + my.options.icons.rider_active_jo, + my.layer_groups.rider_active_jo, + my.options.rider_popup_url, + name + ); + }); } removeRiderMarker(id) { @@ -305,41 +313,40 @@ class DashboardMap { var lng = data.longitude; var name = ''; - if (my.rider_names.hasOwnProperty(id)) { - name = my.rider_names[id]; - - if (data.has_jo) - my.putRiderActiveJOMarker(id, lat, lng, name); - else - my.putRiderAvailableMarker(id, lat, lng, name) - - } else { - getRiderName(id, my.options.rider_name_url, function(name) { - my.rider_names[id] = name; - - if (data.has_jo) - my.putRiderActiveJOMarker(id, lat, lng, name); - else - my.putRiderAvailableMarker(id, lat, lng, name) - }); - } + if (data.has_jo) + my.putRiderActiveJOMarker(id, lat, lng); + else + my.putRiderAvailableMarker(id, lat, lng); }); // console.log(rider_markers); }); } -} -function getRiderName(id, url, callback) { - var name = ''; - var rider_url = url.replace('[id]', id); + getRiderName(id, callback) { + var name = ''; + var rider_url = this.options.rider_name_url.replace('[id]', id); - $.ajax({ - method: "GET", - url: rider_url - }).done(function(response) { - name = response.rider_name; - callback(name); - }); + var my = this; + + console.log('getting rider name for rider ' + id); + + // check if we have it in cache + if (this.rider_names.hasOwnProperty(id)) { + name = this.rider_names[id]; + callback(name); + } else { + // ajax call to get it + $.ajax({ + method: "GET", + url: rider_url + }).done(function(response) { + name = response.rider_name; + + // set name in cache + my.rider_names[id] = name; + callback(name); + }); + } + } } - diff --git a/public/assets/js/map_mqtt.js b/public/assets/js/map_mqtt.js index 63f3e3b3..e3fc9900 100644 --- a/public/assets/js/map_mqtt.js +++ b/public/assets/js/map_mqtt.js @@ -142,6 +142,7 @@ class MapEventHandler { } } else { console.log('rider not in availability check'); + display_marker = false; } // TODO: cache rider availability (available vs active jo) status and check before displaying icon diff --git a/public/assets/js/notification.js b/public/assets/js/notification.js new file mode 100644 index 00000000..9a4b169c --- /dev/null +++ b/public/assets/js/notification.js @@ -0,0 +1,111 @@ +class NotificationHandler { + constructor(options) { + this.options = options; + } + + clearAll() { + // clear notification count + document.getElementById('notif-count').innerHTML = ''; + + // remove notifications + document.getElementById('notif-body').innerHTML = ''; + } + + loadAll() { + console.log('loading notifications'); + // ajax load + var self = this; + var notif_update_url = this.options['notif_ajax_update_url']; + var xhr = new XMLHttpRequest(); + xhr.open('GET', this.options['notif_ajax_url']); + xhr.onload = function() { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + var notifs = data.notifications; + + // update notification unread count + var count_html = data.unread_count; + document.getElementById('notif-count').innerHTML = count_html; + + // do we have any notifications? + if (notifs.length <= 0) + return; + + // add actual notifications + var notif_body = document.getElementById('notif-body'); + var notif_index = 0; + notifs.forEach(function(notif) { + var notif_date = moment(notif.date).fromNow(); + + var notif_html = '
'; + notif_html += ''; + notif_html += ''; + notif_html += '' + notif.text + '' + notif_html += ''; + notif_html += ''; + notif_html += notif_date; + notif_html += ''; + notif_html += '
'; + + notif_body.insertAdjacentHTML('beforeend', notif_html); + + document.getElementsByClassName('m-list-timeline__item')[notif_index].addEventListener('click', function(e) { + e.preventDefault(); + $.ajax({ + method: "POST", + url: notif_update_url, + data: {id: notif.id} + }).done(function(response) { + window.location.href = notif.link; + }); + }); + + notif_index++; + }); + } + + }; + xhr.send(); + } + + listen(user_id, host, port, use_ssl = false) { + var d = new Date(); + var client_id = "dash-" + user_id + "-" + d.getMonth() + "-" + d.getDate() + "-" + d.getHours() + "-" + d.getMinutes() + "-" + d.getSeconds() + "-" + d.getMilliseconds(); + + this.mqtt = new Paho.MQTT.Client(host, port, client_id); + var options = { + useSSL: use_ssl, + timeout: 3, + invocationContext: this, + onSuccess: this.onConnect.bind(this) + } + + this.mqtt.onMessageArrived = this.onMessage.bind(this); + + this.mqtt.connect(options); + } + + onConnect(icontext) { + console.log('notification mqtt connected'); + var my = icontext.invocationContext; + + // subscribe to general notifications + my.mqtt.subscribe('user/0/notification'); + } + + onMessage(msg) { + console.log('notification event received'); + // we don't care about messasge, we update + this.clearAll(); + this.loadAll(); + } + + getIcon(type_id) { + if (type_id in this.options['icons']) { + return this.options['icons'][type_id]; + } + + return this.options['default_icon']; + } + +} diff --git a/src/Controller/NotificationController.php b/src/Controller/NotificationController.php new file mode 100644 index 00000000..64150c51 --- /dev/null +++ b/src/Controller/NotificationController.php @@ -0,0 +1,111 @@ +sub(new DateInterval('PT10M')); + $notifs = [ + [ + 'id' => 1, + 'type' => 'jo_new', + 'date' => $date->format('Y-m-d\TH:i:s.000P'), + 'text' => 'Sample incoming job order', + 'link' => '#', + ], [ + 'id' => 2, + 'type' => 'rider_accept', + 'date' => $date->format('Y-m-d\TH:i:s.000P'), + 'text' => 'Sample rider has accepted job order', + 'link' => '#', + ], [ + 'id' => 3, + 'type' => 'jo_cancel', + 'date' => $date->format('Y-m-d\TH:i:s.000P'), + 'text' => 'Customer has cancelled job order.', + 'link' => '#', + ], [ + 'id' => 3, + 'type' => 'rider_reject', + 'date' => $date->format('Y-m-d\TH:i:s.000P'), + 'text' => 'Rider has rejected job order. Job order needs to be reassigned.', + 'link' => '#', + ], + ]; + $sample_data = [ + 'count' => 4, + 'notifications' => $notifs, + ]; + */ + + $notifs = $em->getRepository(Notification::class)->findBy(['user_id' => 0]); + $notif_data = []; + + $unread_count = 0; + foreach ($notifs as $notif) + { + if (!($notif->isRead())) + $unread_count++; + + $notif_data[] = [ + 'id' => $notif->getID(), + 'type' => 'jo_new', + 'is_read' => $notif->isRead(), + 'is_fresh' => $notif->isFresh(), + 'date' => $notif->getDateCreate()->format('Y-m-d\TH:i:s.000P'), + 'text' => $notif->getMessage(), + 'link' => $notif->getURL(), + ]; + } + + + $sample_data = [ + 'count' => count($notif_data), + 'unread_count' => $unread_count, + 'notifications' => $notif_data, + ]; + + $res = new JsonResponse($sample_data); + + return $res; + } + + // TODO: security + public function ajaxUpdate(EntityManagerInterface $em, Request $req) + { + $notif_id = $req->request->get('id'); + error_log($notif_id); + + $notif = $em->getRepository(Notification::class)->find($notif_id); + + if ($notif != null) + { + // TODO: fresh is if unread and still within x hours + // but for now fresh and unread are both the same + $notif->setIsRead(true); + $notif->setIsFresh(false); + + $em->persist($notif); + $em->flush(); + } + + $res = new JsonResponse(); + + return $res; + } +} diff --git a/src/Entity/Notification.php b/src/Entity/Notification.php new file mode 100644 index 00000000..012bcb15 --- /dev/null +++ b/src/Entity/Notification.php @@ -0,0 +1,148 @@ +date_create = new DateTime(); + $this->flag_read = false; + $this->flag_fresh = true; + } + + public function getID() + { + return $this->id; + } + + public function getDateCreate() + { + return $this->date_create; + } + + public function setUserID($user_id) + { + $this->user_id = $user_id; + return $this; + } + + public function getUserID() + { + return $this->user_id; + } + + public function setIcon($icon) + { + $this->icon = $icon; + return $this; + } + + public function getIcon() + { + return $this->icon; + } + + public function setMessage($message) + { + $this->message = $message; + return $this; + } + + public function getMessage() + { + return $this->message; + } + + public function setURL($url) + { + $this->url = $url; + return $this; + } + + public function getURL() + { + return $this->url; + } + + public function setIsRead($bool = true) + { + $this->flag_read = $bool; + return $this; + } + + public function isRead() + { + return $this->flag_read; + } + + public function setIsFresh($bool = true) + { + $this->flag_fresh = $bool; + return $this; + } + + public function isFresh() + { + return $this->flag_fresh; + } +} diff --git a/src/Service/JobOrderHandler/CMBJobOrderHandler.php b/src/Service/JobOrderHandler/CMBJobOrderHandler.php index 9ffb102a..b55318e1 100644 --- a/src/Service/JobOrderHandler/CMBJobOrderHandler.php +++ b/src/Service/JobOrderHandler/CMBJobOrderHandler.php @@ -449,6 +449,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $jo = $em->getRepository(JobOrder::class)->find($id); $old_jo_status = null; + $old_rider = null; if (empty($jo)) { // new job order @@ -456,7 +457,8 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface } else { - //$old_rider = $jo->getRider(); + // need to get old values of rider and status to see if we need to change JO status or not + $old_rider = $jo->getRider(); $old_jo_status = $jo->getStatus(); } @@ -639,10 +641,25 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface // and JO is already in_transit or in_progress? // retain old jo status if it's an update JO - if ($old_jo_status != null) - $jo->setStatus($old_jo_status); - else + // check old rider if it is also a reassignment + // old_rider should be null if JO has been rejected + if (($old_rider == null) && ($old_jo_status == null)) $jo->setStatus(JOStatus::ASSIGNED); + else + { + error_log('not a new JO'); + $new_rider = $jo->getRider(); + if ($new_rider != $old_rider) + { + // reassignment + $jo->setStatus(JOStatus::ASSIGNED); + } + else + { + if ($old_jo_status != null) + $jo->setStatus($old_jo_status); + } + } // check if user is null, meaning call to create came from API if ($user != null) @@ -702,8 +719,8 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $em->flush(); // check if JO has been reassigned - //if ($old_rider != $rider) - if ($old_jo_status != $jo->getStatus()) + if ($old_rider != $jo->getRider()) + //if ($old_jo_status != $jo->getStatus()) { error_log('JO has been reassigned'); // TODO: refactor later diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php new file mode 100644 index 00000000..86de0348 --- /dev/null +++ b/src/Service/NotificationManager.php @@ -0,0 +1,48 @@ +redis = $redis_prov->getRedisClient(); + $this->redis_mqtt_key = $redis_mqtt_key; + $this->em = $em; + } + + // set user_id to 0 for all + public function sendNotification($user_id, $msg, $url) + { + // send mqtt + $chan = $this->getChannel($user_id); + $data = $chan . '|' . $msg; + $this->redis->lpush($this->redis_mqtt_key, $data); + + // create notif + $notif = new Notification(); + $notif->setUserID($user_id) + ->setIcon('') + ->setMessage($msg) + ->setURL($url); + + // save to db + $this->em->persist($notif); + $this->em->flush(); + } + + protected function getChannel($user_id) + { + return str_replace('{user_id}', $user_id, self::NOTIF_KEY ); + } +} diff --git a/src/Service/RiderAPIHandler/CMBRiderAPIHandler.php b/src/Service/RiderAPIHandler/CMBRiderAPIHandler.php index 282f6bb6..80a8301b 100644 --- a/src/Service/RiderAPIHandler/CMBRiderAPIHandler.php +++ b/src/Service/RiderAPIHandler/CMBRiderAPIHandler.php @@ -5,6 +5,7 @@ namespace App\Service\RiderAPIHandler; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use App\Ramcar\CMBServiceType; use App\Ramcar\TradeInType; @@ -23,6 +24,7 @@ use App\Service\WarrantyHandler; use App\Service\JobOrderHandlerInterface; use App\Service\InvoiceGeneratorInterface; use App\Service\RiderTracker; +use App\Service\NotificationManager; use App\Entity\RiderSession; use App\Entity\Rider; @@ -53,13 +55,16 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface protected $ic; protected $session; protected $upload_dir; + protected $nm; + protected $router; public function __construct(EntityManagerInterface $em, RedisClientProvider $redis, EncoderFactoryInterface $ef, RiderCache $rcache, string $country_code, MQTTClient $mclient, WarrantyHandler $wh, JobOrderHandlerInterface $jo_handler, InvoiceGeneratorInterface $ic, string $upload_dir, - RiderTracker $rider_tracker) + RiderTracker $rider_tracker, NotificationManager $nm, + UrlGeneratorInterface $router) { $this->em = $em; $this->redis = $redis; @@ -72,6 +77,8 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $this->ic = $ic; $this->upload_dir = $upload_dir; $this->rider_tracker = $rider_tracker; + $this->nm = $nm; + $this->router = $router; // one device = one session, since we have control over the devices // when a rider logs in, we just change the rider assigned to the device @@ -208,7 +215,7 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $rider_id = $rider->getID(); // cache rider location (default to hub) // TODO: figure out longitude / latitude default - $this->rcache->addActiveRider($rider_id, 0, 0); + // $this->rcache->addActiveRider($rider_id, 0, 0); // TODO: log rider logging in @@ -278,7 +285,7 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $rider->setActive(false); // remove from cache - $this->rcache->removeActiveRider($rider->getID()); + // $this->rcache->removeActiveRider($rider->getID()); // remove rider from session $this->session->setRider(null); @@ -311,6 +318,11 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $this->em->flush(); + // cache rider location (default to hub) + // TODO: figure out longitude / latitude default + $rider_id = $rider->getID(); + $this->rcache->addActiveRider($rider_id, 0, 0); + // send mqtt event to put rider on map // get rider coordinates from redis $coord = $this->rider_tracker->getRiderLocation($rider->getID()); @@ -360,6 +372,9 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $this->em->flush(); + // remove from cache + $this->rcache->removeActiveRider($rider->getID()); + // send mqtt event to remove rider from map $channel = 'rider/' . $rider->getID() . '/availability'; $payload = [ @@ -1069,6 +1084,10 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $this->em->flush(); + // notification + $notif_url = $this->router->generate('jo_onestep_edit_form', ['id' => $jo->getID()]); + $this->nm->sendNotification(0, 'Job order has been cancelled by rider.', $notif_url); + return $data; } @@ -1154,6 +1173,9 @@ class CMBRiderAPIHandler implements RiderAPIHandlerInterface $this->mclient->publish($channel, $rider_status); + $notif_url = $this->router->generate('jo_onestep_edit_form', ['id' => $jo->getID()]); + $this->nm->sendNotification(0, 'Job order has been rejected by rider.', $notif_url); + return $data; } diff --git a/src/Service/RiderCache.php b/src/Service/RiderCache.php index cdd18864..af5ca716 100644 --- a/src/Service/RiderCache.php +++ b/src/Service/RiderCache.php @@ -2,6 +2,8 @@ namespace App\Service; +use Doctrine\ORM\EntityManagerInterface; + use App\Service\RedisClientProvider; use App\Entity\Rider; @@ -10,12 +12,14 @@ class RiderCache protected $redis; protected $loc_key; protected $status_key; + protected $em; - public function __construct(RedisClientProvider $redis_prov, $loc_key, $status_key) + public function __construct(EntityManagerInterface $em, RedisClientProvider $redis_prov, $loc_key, $status_key) { $this->redis = $redis_prov->getRedisClient(); $this->loc_key = $loc_key; $this->status_key = $status_key; + $this->em = $em; } public function addActiveRider($id, $lat, $lng) @@ -46,10 +50,15 @@ class RiderCache $lng = $data[1][0]; $lat = $data[1][1]; - $locs[$id] = [ - 'longitude' => $lng, - 'latitude' => $lat, - ]; + // get rider details so we can check for availability + $rider = $this->getRiderDetails($id); + if ($rider != null) + { + $locs[$id] = [ + 'longitude' => $lng, + 'latitude' => $lat, + ]; + } } // error_log(print_r($all_riders, true)); @@ -73,4 +82,17 @@ class RiderCache { $this->redis->hincrby($this->status_key, $id, -1); } + + protected function getRiderDetails($id) + { + $rider = $this->em->getRepository(Rider::class)->find($id); + if ($rider == null) + return null; + + // return only if available + if ($rider->isAvailable()) + return $rider; + + return null; + } } diff --git a/templates/base.html.twig b/templates/base.html.twig index 5e5abfc2..37fcfc4d 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -448,217 +448,50 @@
+ + @@ -770,6 +605,26 @@ + + + + {% block scripts %}{% endblock %} diff --git a/utils/clear_jo_data.sql b/utils/clear_jo_data.sql new file mode 100644 index 00000000..30b20c61 --- /dev/null +++ b/utils/clear_jo_data.sql @@ -0,0 +1,15 @@ +delete from jo_event; +delete from invoice_item; +delete from invoice; +delete from ticket; + +set foreign_key_checks = 0; +delete from job_order; +set foreign_key_checks = 1; + +delete from mobile_session; +delete from customer_vehicle; +delete from customer; +delete from warranty; + +update rider set active_jo_id = null;