Resolve "Transition branch for CMB and Resq merging" #1181

Merged
korina.cordero merged 258 commits from 329-transition-branch-for-cmb-and-resq-merging into master 2020-04-03 02:49:26 +00:00
104 changed files with 19774 additions and 5867 deletions
Showing only changes of commit 2093bbde92 - Show all commits

View file

@ -28,6 +28,8 @@ RT_SHORTCODE=1234
MQTT_IP_ADDRESS=localhost MQTT_IP_ADDRESS=localhost
MQTT_PORT=8883 MQTT_PORT=8883
MQTT_CERT=/location/of/cert/file.crt MQTT_CERT=/location/of/cert/file.crt
MQTT_WS_HOST=insertiphere
MQTT_WS_PORT=8083
# redis client # redis client
REDIS_CLIENT_SCHEME=tcp REDIS_CLIENT_SCHEME=tcp
@ -50,3 +52,6 @@ GEOFENCE_ENABLE=settotrueorfalse
CVU_MFG_ID=insertmfgidforunknownvehicles CVU_MFG_ID=insertmfgidforunknownvehicles
CVU_BRAND_ID=insertbrandidforunknownvehicles CVU_BRAND_ID=insertbrandidforunknownvehicles
# country code prefix
COUNTRY_CODE=+insertcountrycodehere

1
.gitignore vendored
View file

@ -7,6 +7,7 @@
/sql/ /sql/
/pem/ /pem/
/migration/ /migration/
/kml/
###< symfony/framework-bundle ### ###< symfony/framework-bundle ###
*.swp *.swp

View file

@ -17,6 +17,7 @@
"predis/predis": "^1.1", "predis/predis": "^1.1",
"sensio/framework-extra-bundle": "^5.1", "sensio/framework-extra-bundle": "^5.1",
"setasign/fpdf": "^1.8", "setasign/fpdf": "^1.8",
"symfony/asset": "^4.0",
"symfony/console": "^4.0", "symfony/console": "^4.0",
"symfony/debug": "^4.0", "symfony/debug": "^4.0",
"symfony/filesystem": "^4.0", "symfony/filesystem": "^4.0",

62
composer.lock generated
View file

@ -1,10 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "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" "This file is @generated automatically"
], ],
"content-hash": "4873ae3fd18db755bc9bf395bbbfb141", "content-hash": "b101ecfbc1f6f2270f0e8ad326035b7e",
"packages": [ "packages": [
{ {
"name": "catalyst/auth-bundle", "name": "catalyst/auth-bundle",
@ -2411,6 +2411,62 @@
], ],
"time": "2016-01-01T17:47:15+00:00" "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", "name": "symfony/cache",
"version": "v4.3.1", "version": "v4.3.1",
@ -5344,6 +5400,7 @@
"code", "code",
"zf2" "zf2"
], ],
"abandoned": "laminas/laminas-code",
"time": "2018-08-13T20:36:59+00:00" "time": "2018-08-13T20:36:59+00:00"
}, },
{ {
@ -5398,6 +5455,7 @@
"events", "events",
"zf2" "zf2"
], ],
"abandoned": "laminas/laminas-eventmanager",
"time": "2018-04-25T15:33:34+00:00" "time": "2018-04-25T15:33:34+00:00"
} }
], ],

View file

@ -242,6 +242,10 @@ access_keys:
label: Edit label: Edit
- id: joborder.cancel - id: joborder.cancel
label: Cancel label: Cancel
- id: jo_onestep.form
label: One-step Process
- id: jo_onestep.edit
label: One-step Process Edit
- id: support - id: support
label: Customer Support Access label: Customer Support Access

View file

@ -98,6 +98,10 @@ main_menu:
acl: joborder.menu acl: joborder.menu
label: Job Order label: Job Order
icon: flaticon-calendar-3 icon: flaticon-calendar-3
- id: jo_onestep_form
acl: jo_onestep.form
label: One-step Process
parent: joborder
- id: jo_in - id: jo_in
acl: jo_in.list acl: jo_in.list
label: Incoming label: Incoming

View file

@ -21,6 +21,11 @@ security:
methods: [GET] methods: [GET]
security: false security: false
tracker:
pattern: ^\/track\/
methods: [GET]
security: false
api: api:
pattern: ^\/api\/ pattern: ^\/api\/
security: false security: false

View file

@ -4,3 +4,5 @@ twig:
strict_variables: '%kernel.debug%' strict_variables: '%kernel.debug%'
globals: globals:
gmaps_api_key: "%env(GMAPS_API_KEY)%" gmaps_api_key: "%env(GMAPS_API_KEY)%"
mqtt_host: "%env(MQTT_WS_HOST)%"
mqtt_port: "%env(MQTT_WS_PORT)%"

View file

@ -3,7 +3,3 @@
# controller: App\Controller\DefaultController::index # controller: App\Controller\DefaultController::index
# #
# #
home:
path: /
controller: App\Controller\HomeController::index

8
config/routes/home.yaml Normal file
View file

@ -0,0 +1,8 @@
home:
path: /
controller: App\Controller\HomeController::index
rider_locations:
path: /rider_locations
controller: App\Controller\HomeController::getRiderLocations

View file

@ -34,3 +34,12 @@ hub_delete:
controller: App\Controller\HubController::destroy controller: App\Controller\HubController::destroy
methods: [DELETE] methods: [DELETE]
hub_nearest:
path: /ajax/nearest_hubs
controller: App\Controller\HubController::nearest
methods: [GET]
hub_riders:
path: /ajax/hubs/riders
controller: App\Controller\HubController::getHubRiders
methods: [GET]

View file

@ -175,3 +175,34 @@ jo_reject_hub:
path: /job-order/{id}/reject-hub path: /job-order/{id}/reject-hub
controller: App\Controller\JobOrderController::rejectHubSubmit controller: App\Controller\JobOrderController::rejectHubSubmit
methods: [POST] methods: [POST]
jo_onestep_form:
path: /job-order/onestep
controller: App\Controller\JobOrderController::oneStepForm
methods: [GET]
jo_onestep_submit:
path: /job-order/onestep
controller: App\Controller\JobOrderController::oneStepSubmit
methods: [POST]
jo_onestep_edit_form:
path: /job-order/onestep/{id}/edit
controller: App\Controller\JobOrderController::oneStepEditForm
methods: [GET]
jo_onestep_edit_submit:
path: /job-order/onestep/{id}/edit
controller: App\Controller\JobOrderController::oneStepEditSubmit
methods: [POST]
jo_ajax_popup:
path: /job-order/{id}/popup
controller: App\Controller\JobOrderController::popupInfo
methods: [GET]
jo_tracker:
path: /track/{id}
controller: App\Controller\JobOrderController::tracker
methods: [GET]

View file

@ -36,3 +36,8 @@ rider_delete:
path: /riders/{id} path: /riders/{id}
controller: App\Controller\RiderController::destroy controller: App\Controller\RiderController::destroy
methods: [DELETE] methods: [DELETE]
rider_ajax_popup:
path: /riders/{id}/popup
controller: App\Controller\RiderController::popupInfo
methods: [GET]

View file

@ -76,6 +76,7 @@ services:
App\Service\MQTTClient: App\Service\MQTTClient:
arguments: arguments:
$redis_client: "@App\\Service\\RedisClientProvider" $redis_client: "@App\\Service\\RedisClientProvider"
$key: "mqtt_events"
App\Service\APNSClient: App\Service\APNSClient:
arguments: arguments:
@ -87,7 +88,6 @@ services:
$host: "%env(REDIS_CLIENT_HOST)%" $host: "%env(REDIS_CLIENT_HOST)%"
$port: "%env(REDIS_CLIENT_PORT)%" $port: "%env(REDIS_CLIENT_PORT)%"
$password: "%env(REDIS_CLIENT_PASSWORD)%" $password: "%env(REDIS_CLIENT_PASSWORD)%"
$env_flag: "dev"
App\Service\GeofenceTracker: App\Service\GeofenceTracker:
arguments: arguments:
@ -108,6 +108,11 @@ services:
$cvu_mfg_id: "%env(CVU_MFG_ID)%" $cvu_mfg_id: "%env(CVU_MFG_ID)%"
$cvu_brand_id: "%env(CVU_BRAND_ID)%" $cvu_brand_id: "%env(CVU_BRAND_ID)%"
# rider tracker service
App\Service\RiderTracker:
arguments:
$redis_client: "@App\\Service\\RedisClientProvider"
Catalyst\APIBundle\Security\APIKeyUserProvider: Catalyst\APIBundle\Security\APIKeyUserProvider:
arguments: arguments:
$em: "@doctrine.orm.entity_manager" $em: "@doctrine.orm.entity_manager"
@ -151,3 +156,77 @@ services:
$menu_name: "main_menu" $menu_name: "main_menu"
tags: tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController } - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
# invoice generator
App\Service\InvoiceGenerator\CMBInvoiceGenerator: ~
# invoice generator interface
App\Service\InvoiceGeneratorInterface: "@App\\Service\\InvoiceGenerator\\CMBInvoiceGenerator"
#App\Service\InvoiceGeneratorInterface: "@App\\Service\\InvoiceGenerator\\ResqInvoiceGenerator"
# job order generator
#App\Service\JobOrderHandler\ResqJobOrderHandler:
# arguments:
# $country_code: "%env(COUNTRY_CODE)%"
App\Service\JobOrderHandler\CMBJobOrderHandler:
arguments:
$country_code: "%env(COUNTRY_CODE)%"
#job order generator interface
App\Service\JobOrderHandlerInterface: "@App\\Service\\JobOrderHandler\\CMBJobOrderHandler"
#App\Service\JobOrderHandlerInterface: "@App\\Service\\JobOrderHandler\\ResqJobOrderHandler"
# customer generator
App\Service\CustomerHandler\CMBCustomerHandler:
arguments:
$country_code: "%env(COUNTRY_CODE)%"
#App\Service\CustomerHandler\ResqCustomerHandler:
# arguments:
# $country_code: "%env(COUNTRY_CODE)%"
# customer generator interface
App\Service\CustomerHandlerInterface: "@App\\Service\\CustomerHandler\\CMBCustomerHandler"
#App\Service\CustomerHandlerInterface: "@App\\Service\\CustomerHandler\\ResqCustomerHandler"
# rider assignment
App\Service\RiderAssignmentHandler\CMBRiderAssignmentHandler: ~
# rider assignment interface
App\Service\RiderAssignmentHandlerInterface: "@App\\Service\\RiderAssignmentHandler\\CMBRiderAssignmentHandler"
# map manager
#App\Service\GISManager\Bing: ~
App\Service\GISManager\OpenStreet: ~
#App\Service\GISManager\Google: ~
#App\Service\GISManagerInterface: "@App\\Service\\GISManager\\Bing"
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)%"

View file

@ -0,0 +1,8 @@
DELETE FROM battery;
DELETE FROM battery_manufacturer;
DELETE FROM battery_manufacturer;
DELETE FROM battery_model;
DELETE FROM battery_size;
DELETE FROM vehicle;
DELETE FROM vehicle_manufacturer;
DELETE FROM battery_vehicle;

View file

@ -295,3 +295,67 @@ span.has-danger,
.btn-icon { .btn-icon {
margin-right: .5em; margin-right: .5em;
} }
.marker-pin {
width: 30px;
height: 30px;
border-radius: 50% 50% 50% 0;
background: #c30b82;
position: absolute;
transform: rotate(-45deg);
left: 50%;
top: 50%;
margin: -15px 0 0 -15px;
}
.marker-pin::after {
content: '';
width: 24px;
height: 24px;
margin: 3px 0 0 3px;
background: #fff;
position: absolute;
border-radius: 50%;
}
.map-div-icon i {
position: absolute;
width: 22px;
font-size: 22px;
left: 0;
right: 0;
margin: 10px auto;
text-align: center;
}
.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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -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 &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
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);
});
}
}

View file

@ -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;
}
}
}
}

View file

@ -0,0 +1,315 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\Common\Persistence\ObjectManager;
use App\Entity\Battery;
use App\Entity\BatteryManufacturer;
use App\Entity\BatteryModel;
use App\Entity\BatterySize;
class ImportCMBBatteryDataCommand extends Command
{
const F_BATT_CODE = 1;
const F_BATT_DESC = 2;
const F_BATT_PRICE = 3;
protected $em;
protected $bmanu_hash;
protected $bmodel_hash;
protected $bsize_hash;
protected $batt_hash;
public function __construct(ObjectManager $om)
{
$this->em = $om;
// load existing batteries and sizes
$this->loadBatteryManufacturers();
$this->loadBatteryModels();
$this->loadBatteries();
$this->loadBatterySizes();
parent::__construct();
}
protected function configure()
{
$this->setName('cmbbatterydata:import')
->setDescription('Import a CSV file with battery data.')
->setHelp('Adds the battery data based on imported CSV.')
->addArgument('file', InputArgument::REQUIRED, 'Path to the CSV file.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$csv_file = $input->getArgument('file');
// attempt to open file
try
{
$fh = fopen($csv_file, "r");
}
catch (Exception $e)
{
throw new Exception('The file "' . $csv_file . '" could be read.');
}
// get entity manager
$em = $this->em;
// loop through the rows
$row_num = 0;
error_log('Processing battery csv file...');
while (($fields = fgetcsv($fh)) !== false)
{
// data starts at row 2
if ($row_num < 2)
{
$row_num++;
continue;
}
// battery info
$code = trim($fields[self::F_BATT_CODE]);
$desc = trim($fields[self::F_BATT_DESC]);
$price = trim($fields[self::F_BATT_PRICE]);
$clean_price = trim($price, '$');
$battery_info = explode(' ', $desc);
// if battery_info has 3 elements, get the last two
// [0] = battery manufacturer
// [1] = battery model
// [2] = battery size
// if only 2, get both
// [0] = battery manufacturer and battery model
// [1] = battery size
// if 4,
// [0] = battery manufacturer
// concatenate [1] and [2] for the battery model
// [3] = battery size
$battery_manufacturer = '';
$battery_model = '';
$battery_size = '';
if (count($battery_info) == 3)
{
// sample: Century Marathoner 120-7L
$battery_manufacturer = trim($battery_info[0]);
$battery_model = trim($battery_info[1]);
$battery_size = trim($battery_info[2]);
}
if (count($battery_info) == 2)
{
// sample: Marshall DIN55R
$battery_manufacturer = trim($battery_info[0]);
$battery_model = trim($battery_info[0]);
$battery_size = trim($battery_info[1]);
}
if (count($battery_info) == 4)
{
// sample: Motolite Classic Wetcharged DIN100L
$battery_manufacturer = trim($battery_info[0]);
$battery_model = trim($battery_info[1]) . ' ' . trim($battery_info[2]);
$battery_size = trim($battery_info[3]);
}
// check if battery size has ()
// if so, trim it to ignore the parenthesis and what's after (.
$pos = stripos($battery_size, '(');
if ($pos == true)
{
$sizes = explode('(', $battery_size);
$clean_size = trim($sizes[0]);
}
else
{
$clean_size = $battery_size;
}
//error_log('battery manufacturer ' . $battery_manufacturer);
//error_log('battery model ' . $battery_model);
//error_log('battery size ' . $battery_size);
// normalize the manufacturer, model and size for the hash
// when we add to db for manufacturer, model, and size, we do not use the normalized versions
$normalized_manu = $this->normalizeName($battery_manufacturer);
$normalized_model = $this->normalizeName($battery_model);
$normalized_size = $this->normalizeName($clean_size);
// save battery manufacturer if not yet in system
if (!isset($this->bmanu_hash[$normalized_manu]))
{
$this->addBatteryManufacturer($battery_manufacturer);
}
// save battery model if not yet in system
if (!isset($this->bmodel_hash[$normalized_model]))
{
$this->addBatteryModel($battery_model);
}
// save battery size if not yet in system
if (!isset($this->bsize_hash[$normalized_size]))
{
$this->addBatterySize($clean_size);
}
// save battery if not yet in system
if (!isset($this->batt_hash[$normalized_manu][$normalized_model][$normalized_size]))
{
$this->addBattery($normalized_manu, $normalized_model, $normalized_size, $code, $clean_price);
}
}
}
protected function addBatteryManufacturer($name)
{
$batt_manufacturer = new BatteryManufacturer();
$batt_manufacturer->setName($name);
$this->em->persist($batt_manufacturer);
$this->em->flush();
// add new manufacturer to hash
$normalized_name = $this->normalizeName($name);
$this->bmanu_hash[$normalized_name] = $batt_manufacturer;
}
protected function addBatteryModel($name)
{
$batt_model = new BatteryModel();
$batt_model->setName($name);
$this->em->persist($batt_model);
$this->em->flush();
// add new model to hash
$normalized_name = $this->normalizeName($name);
$this->bmodel_hash[$normalized_name] = $batt_model;
}
protected function addBatterySize($name)
{
if (!empty($name))
{
// save to db
$batt_size = new BatterySize();
$batt_size->setName($name);
$this->em->persist($batt_size);
$this->em->flush();
// add new size into hash
$normalized_name = $this->normalizeName($name);
$this->bsize_hash[$normalized_name] = $batt_size;
}
}
protected function addBattery($manufacturer, $brand, $size, $code, $price)
{
// save to db
$bmanu = $this->bmanu_hash[$manufacturer];
$bmodel = $this->bmodel_hash[$brand];
$bsize = $this->bsize_hash[$size];
$battery = new Battery();
$battery->setManufacturer($bmanu)
->setModel($bmodel)
->setSize($bsize)
->setWarrantyPrivate(21)
->setWarrantyCommercial(6)
->setWarrantyTnv(12)
->setProductCode($code)
->setSAPCode($code)
->setSellingPrice($price);
$this->em->persist($battery);
$this->em->flush();
// insert into hash
$this->batt_hash[$brand][$brand][$size] = $battery;
// add battery into battery manufacturer, battery model, and battery size
$bmanu->addBattery($battery);
$bmodel->addBattery($battery);
$bsize->addBattery($battery);
$this->em->persist($bmanu);
$this->em->persist($bmodel);
$this->em->persist($bsize);
$this->em->flush();
}
protected function loadBatteryManufacturers()
{
$this->bmanu_hash = [];
$batt_manufacturers = $this->em->getRepository(BatteryManufacturer::class)->findAll();
foreach ($batt_manufacturers as $batt_manu)
{
$name = $this->normalizeName($batt_manu->getName());
$this->bmanu_hash[$name] = $batt_manu;
}
}
protected function loadBatteryModels()
{
$this->bmodel_hash = [];
$batt_models = $this->em->getRepository(BatteryModel::class)->findAll();
foreach ($batt_models as $batt_model)
{
$name = $this->normalizeName($batt_model->getName());
$this->bmodel_hash[$name] = $batt_model;
}
}
protected function loadBatterySizes()
{
$this->bsize_hash = [];
$batt_sizes = $this->em->getRepository(BatterySize::class)->findAll();
foreach ($batt_sizes as $batt_size)
{
$name = $this->normalizeName($batt_size->getName());
$this->bsize_hash[$name] = $batt_size;
}
}
protected function loadBatteries()
{
$this->batt_hash = [];
$batts = $this->em->getRepository(Battery::class)->findAll();
foreach ($batts as $batt)
{
$brand = $this->normalizeName($batt->getManufacturer()->getName());
$model = $this->normalizeName($batt->getModel()->getName());
$size = $this->normalizeName($batt->getSize()->getName());
$this->batt_hash[$brand][$model][$size] = $batt;
}
}
protected function normalizeName($name)
{
$normalized_key = trim(strtolower($name));
return $normalized_key;
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\Common\Persistence\ObjectManager;
use App\Entity\BatterySize;
class ImportCMBBatteryTradeInPriceCommand extends Command
{
const F_SIZE_DESC = 2;
const F_TRADEIN_PRICE = 3;
protected $em;
protected $bsize_hash;
public function __construct(ObjectManager $om)
{
$this->em = $om;
// load existing sizes
$this->loadBatterySizes();
parent::__construct();
}
protected function configure()
{
$this->setName('cmbbatterydata:importtradeinprice')
->setDescription('Import a CSV file with trade in prices.')
->setHelp('Adds the battery tradein prices to existing batteries based on imported CSV.')
->addArgument('file', InputArgument::REQUIRED, 'Path to the CSV file.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$csv_file = $input->getArgument('file');
// attempt to open file
try
{
$fh = fopen($csv_file, "r");
}
catch (Exception $e)
{
throw new Exception('The file "' . $csv_file . '" could be read.');
}
// get entity manager
$em = $this->em;
// loop through the rows
$row_num = 0;
error_log('Processing battery tradein price csv file...');
while (($fields = fgetcsv($fh)) !== false)
{
// data starts at row 2
if ($row_num < 2)
{
$row_num++;
continue;
}
// tradein price info
// battery price info
$desc = trim($fields[self::F_SIZE_DESC]);
$price = trim($fields[self::F_TRADEIN_PRICE]);
$clean_price = trim($price, '$');
$size_info = explode(' ', $desc);
$size = $size_info[1];
if (isset($this->bsize_hash[$size]))
{
$battery_size = $this->bsize_hash[$size];
// use TIPriceMotolite
$battery_size->setTIPriceMotolite($clean_price);
$this->em->persist($battery_size);
$this->em->flush();
}
else
{
error_log('Cannot find battery size ' . $size);
}
}
}
protected function loadBatterySizes()
{
$this->bsize_hash = [];
$batt_sizes = $this->em->getRepository(BatterySize::class)->findAll();
foreach ($batt_sizes as $batt_size)
{
$name = $batt_size->getName();
$this->bsize_hash[$name] = $batt_size;
}
}
}

View file

@ -0,0 +1,447 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\Common\Persistence\ObjectManager;
use App\Entity\BatteryManufacturer;
use App\Entity\BatteryModel;
use App\Entity\BatterySize;
use App\Entity\Battery;
use App\Entity\VehicleManufacturer;
use App\Entity\Vehicle;
class ImportCMBVehicleCompatibilityCommand extends Command
{
// field index in csv file
const F_VEHICLE_MANUFACTURER = 1;
const F_VEHICLE_MAKE = 2;
const F_VEHICLE_YEAR = 3;
const F_BATT_SDFC = 4;
const F_BATT_ULTRAMAX = 5;
const F_BATT_MOTOLITE = 6;
const F_BATT_MARATHONER = 7;
const F_BATT_EXCEL = 8;
const STR_CENTURY = 'Century';
const STR_MOTOLITE = 'Motolite';
const STR_MARSHALL = 'Marshall';
const STR_SDFC = 'SDFC';
const STR_MARATHONER = 'Marathoner';
const STR_WETCHARGED = 'Classic WetCharged';
const STR_ULTRAMAX = 'ULTRAMAX';
const STR_EXCEL = 'Excel';
const STR_PRESENT = 'Present';
const STR_M_42 = 'M-42';
const STR_M42 = 'M42';
protected $em;
protected $bmanu_hash;
protected $bmodel_hash;
protected $bsize_hash;
protected $batt_hash;
protected $vmanu_hash;
protected $vmake_hash;
public function __construct(ObjectManager $om)
{
$this->em = $om;
// load existing battery data
$this->loadBatteryManufacturers();
$this->loadBatteryModels();
$this->loadBatterySizes();
$this->loadBatteries();
// load existing vehicle data
$this->loadVehicleManufacturers();
$this->loadVehicleMakes();
parent::__construct();
}
protected function configure()
{
$this->setName('cmbvehiclecompatibility:import')
->setDescription('Retrieve from a CSV file battery and vehicle information.')
->setHelp('Creates battery manufacturers, models, sizes, vehicle makes, and models based on data from imported CSV.')
->addArgument('file', InputArgument::REQUIRED, 'Path to the CSV file.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$csv_file = $input->getArgument('file');
// attempt to open file
try
{
$fh = fopen($csv_file, "r");
}
catch (Exception $e)
{
throw new Exception('The file "' . $csv_file . '" could be read.');
}
// get entity manager
$em = $this->em;
$row_num = 0;
error_log('Processing vehicle compatibility csv file...');
while (($fields = fgetcsv($fh)) !== false)
{
$comp_batteries = [];
if ($row_num < 2)
{
$row_num++;
continue;
}
// initialize size battery array for cases where the battery size has '/'
$sdfc_sizes = [];
$ultramax_sizes = [];
$motolite_sizes = [];
$marathoner_sizes = [];
$excel_sizes = [];
// battery info
$sdfc_size = trim($fields[self::F_BATT_SDFC]);
$ultramax_size = trim($fields[self::F_BATT_ULTRAMAX]);
$motolite_size = trim($fields[self::F_BATT_MOTOLITE]);
$marathoner_size = trim($fields[self::F_BATT_MARATHONER]);
$excel_size = trim($fields[self::F_BATT_EXCEL]);
// check the sizes for '/'
$pos = stripos($sdfc_size, '/');
if ($pos == false)
{
// no '/' in size
$sdfc_sizes[] = $this->normalizeName($sdfc_size);
}
else
{
// we have '/' in size so we have to explode
$sizes = explode('/', $sdfc_size);
foreach ($sizes as $size)
{
$sdfc_sizes[] = $this->normalizeName($size);
}
}
$pos = stripos($motolite_size, '/');
if ($pos == false)
{
// no '/' in size
$motolite_sizes[] = $this->normalizeName($motolite_size);
}
else
{
// we have '/' in size so we have to explode
$sizes = explode('/', $motolite_size);
foreach ($sizes as $size)
{
$motolite_sizes[] = $this->normalizeName($size);
}
}
$pos = stripos($marathoner_size, '/');
if ($pos == false)
{
// no '/' in size
$marathoner_sizes[] = $this->normalizeName($marathoner_size);
}
else
{
// we have '/' in size so we have to explode
$sizes = explode('/', $marathoner_size);
foreach ($sizes as $size)
{
$marathoner_sizes[] = $this->normalizeName($size);
}
}
$pos = stripos($ultramax_size, '/');
if ($pos == false)
{
// no '/' in size
$ultramax_sizes[] = $this->normalizeName($ultramax_size);
}
else
{
// we have '/' in size so we have to explode
$sizes = explode('/', $ultramax_size);
foreach ($sizes as $size)
{
$ultramax_sizes[] = $this->normalizeName($size);
}
}
$pos = stripos($excel_size, '/');
if ($pos == false)
{
// no '/' in size
$excel_sizes[] = $this->normalizeName($excel_size);
}
else
{
// we have '/' in size so we have to explode
$sizes = explode('/', $excel_size);
foreach ($sizes as $size)
{
$excel_sizes[] = $this->normalizeName($size);
}
}
// normalize the battery manufacturers and battery models
$norm_century = $this->normalizeName(self::STR_CENTURY);
$norm_sdfc = $this->normalizeName(self::STR_SDFC);
$norm_motolite = $this->normalizeName(self::STR_MOTOLITE);
$norm_wetcharged = $this->normalizeName(self::STR_WETCHARGED);
$norm_marathoner = $this->normalizeName(self::STR_MARATHONER);
$norm_ultramax = $this->normalizeName(self::STR_ULTRAMAX);
$norm_excel = $this->normalizeName(self::STR_EXCEL);
//foreach($sdfc_sizes as $size)
//{
// error_log('sdfc size ' . $size);
//}
//foreach($motolite_sizes as $size)
//{
// error_log('motolite size ' . $size);
//}
//foreach($marathoner_sizes as $size)
//{
// error_log('marathoner size ' . $size);
//}
// vehicle info
$manufacturer = trim($fields[self::F_VEHICLE_MANUFACTURER]);
$make = trim($fields[self::F_VEHICLE_MAKE]);
$year = trim($fields[self::F_VEHICLE_YEAR]);
// vehicle data
// check if vehicle manufacturer has been added
if (!isset($this->vmanu_hash[$manufacturer]))
$this->addVehicleManufacturer($manufacturer);
// check if vehicle make has been added
if (!isset($this->vmake_hash[$manufacturer][$make]))
{
foreach($sdfc_sizes as $size)
{
if (!(empty($size)))
{
if (isset($this->batt_hash[$norm_century][$norm_sdfc][$size]))
$comp_batteries[] = $this->batt_hash[$norm_century][$norm_sdfc][$size];
else
error_log('Not in the system: ' . $norm_century . ' ' . $norm_sdfc . ' ' . $size);
}
}
foreach($ultramax_sizes as $size)
{
if (!(empty($size)))
{
if (isset($this->batt_hash[$norm_ultramax][$norm_ultramax][$size]))
$comp_batteries[] = $this->batt_hash[$norm_ultramax][$norm_ultramax][$size];
else
error_log('Not in the system: ' . $norm_ultramax . ' ' . $norm_ultramax . ' ' . $size);
}
}
foreach($motolite_sizes as $size)
{
if (!(empty($size)))
{
if (isset($this->batt_hash[$norm_motolite][$norm_wetcharged][$size]))
$comp_batteries[] = $this->batt_hash[$norm_motolite][$norm_wetcharged][$size];
else
error_log('Not in the system: ' . $norm_motolite . ' ' . $norm_wetcharged . ' ' . $size);
}
}
foreach($marathoner_sizes as $size)
{
if (!(empty($size)))
{
if (isset($this->batt_hash[$norm_century][$norm_marathoner][$size]))
$comp_batteries[] = $this->batt_hash[$norm_century][$norm_marathoner][$size];
else
error_log('Not in the system: ' . $norm_century . ' ' . $norm_marathoner . ' ' . $size);
}
}
foreach($excel_sizes as $size)
{
if (!(empty($size)))
{
if (isset($this->batt_hash[$norm_excel][$norm_excel][$size]))
$comp_batteries[] = $this->batt_hash[$norm_excel][$norm_excel][$size];
else
error_log('Not in the system: ' . $norm_excel . ' ' . $norm_excel . ' ' . $size);
}
}
$this->addVehicleMake($manufacturer, $make, $year, $comp_batteries);
}
$row_num++;
}
}
protected function addVehicleManufacturer($name)
{
// save to db
$vehicle_manufacturer = new VehicleManufacturer();
$vehicle_manufacturer->setName($name);
$this->em->persist($vehicle_manufacturer);
$this->em->flush();
// add to hash
$this->vmanu_hash[$name] = $vehicle_manufacturer;
}
protected function addVehicleMake($manufacturer, $make, $year, $batteries)
{
// save to db
$vehicle = new Vehicle();
$vmanu = $this->vmanu_hash[$manufacturer];
// parse year from and year to
$year_from = '';
$year_to = '';
if (!empty($year))
{
$model_years = explode('-', $year);
$year_from = $model_years[0];
if (!empty($year_to))
$year_to = $model_years[1];
// check if $year_to is the string "Present"
// if so, set to 0, for now
if ($year_to == self::STR_PRESENT)
$year_to = 0;
}
$vehicle->setManufacturer($vmanu)
->setMake($make)
->setModelYearFrom($year_from)
->setModelYearTo($year_to);
// add vehicle to battery
foreach ($batteries as $battery)
{
$battery->addVehicle($vehicle);
$this->em->persist($battery);
}
// add vehicle to manufacturer
$vmanu->addVehicle($vehicle);
$this->em->persist($vmanu);
$this->em->persist($vehicle);
$this->em->flush();
// add to hash
$this->vmake_hash[$manufacturer][$make] = $vehicle;
}
protected function loadBatteryManufacturers()
{
$this->bmanu_hash = [];
$batt_manufacturers = $this->em->getRepository(BatteryManufacturer::class)->findAll();
foreach ($batt_manufacturers as $batt_manu)
{
$name = $this->normalizeName($batt_manu->getName());
$this->bmanu_hash[$name] = $batt_manu;
}
}
protected function loadBatteryModels()
{
$this->bmodel_hash = [];
$batt_models = $this->em->getRepository(BatteryModel::class)->findAll();
foreach ($batt_models as $batt_model)
{
$name = $this->normalizeName($batt_model->getName());
$this->bmodel_hash[$name] = $batt_model;
}
}
protected function loadBatterySizes()
{
$this->bsize_hash = [];
$batt_sizes = $this->em->getRepository(BatterySize::class)->findAll();
foreach ($batt_sizes as $batt_size)
{
$name = $this->normalizeName($batt_size->getName());
$this->bsize_hash[$name] = $batt_size;
}
}
protected function loadBatteries()
{
$this->batt_hash = [];
$batts = $this->em->getRepository(Battery::class)->findAll();
foreach ($batts as $batt)
{
$brand = $this->normalizeName($batt->getManufacturer()->getName());
$model = $this->normalizeName($batt->getModel()->getName());
$size = $this->normalizeName($batt->getSize()->getName());
$this->batt_hash[$brand][$model][$size] = $batt;
}
}
protected function loadVehicleManufacturers()
{
$this->vmanu_hash = [];
$vmanus = $this->em->getRepository(VehicleManufacturer::class)->findAll();
foreach ($vmanus as $vmanu)
{
$name = $vmanu->getName();
$this->vmanu_hash[$name] = $vmanu;
}
}
protected function loadVehicleMakes()
{
$this->vmake_hash = [];
$vmakes = $this->em->getRepository(Vehicle::class)->findAll();
foreach ($vmakes as $vmake)
{
$manufacturer = $vmake->getManufacturer()->getName();
$make = $vmake->getMake();
$this->vmake_hash[$manufacturer][$make] = $vmake;
}
}
protected function normalizeName($name)
{
// check for M-42. Need to convert to M42
if (strcasecmp($name, self::STR_M_42) == 0)
{
$normalized_key = strtolower(self::STR_M42);
}
else
{
$normalized_key = trim(strtolower($name));
}
return $normalized_key;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\DBAL\Connection;
use Doctrine\Common\Persistence\ObjectManager;
use App\Service\JobOrderCache;
use App\Entity\JobOrder;
use App\Ramcar\JOStatus;
use DateTime;
class RefreshJobOrderCacheCommand extends Command
{
protected $em;
protected $jo_cache;
public function __construct(ObjectManager $om, JobOrderCache $jo_cache)
{
$this->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);
}
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace App\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\Common\Persistence\ObjectManager;
use App\Service\RedisClientProvider;
use App\Entity\RiderSession;
class SeedRiderSessionsCommand extends Command
{
protected $em;
protected $redis;
public function __construct(ObjectManager $om, RedisClientProvider $redis)
{
$this->em = $om;
$this->redis = $redis->getRedisClient();
parent::__construct();
}
protected function configure()
{
$this->setName('rider:session:seed')
->setDescription('Seed current rider sessions')
->setHelp('Seed current rider sessions');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// get all rider sessions
$r_sessions = $this->em->getRepository(RiderSession::class)->findAll();
foreach ($r_sessions as $session)
{
// get session id
$session_id = $session->getID();
// get rider id
if ($session->getRider() != null)
{
$rider_id = $session->getRider()->getID();
// key for redis
$redis_key = 'rider.id.' . $session_id;
//$output->writeln('key: ' . $redis_key);
// set to redis cache
$this->redis->set($redis_key, $rider_id);
}
}
}
}

View file

@ -24,7 +24,7 @@ use App\Ramcar\TransactionOrigin;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\JOEventType; use App\Ramcar\JOEventType;
use App\Service\InvoiceCreator; use App\Service\InvoiceGeneratorInterface;
use App\Service\RisingTideGateway; use App\Service\RisingTideGateway;
use App\Service\MQTTClient; use App\Service\MQTTClient;
use App\Service\GeofenceTracker; use App\Service\GeofenceTracker;
@ -817,7 +817,7 @@ class APIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function requestJobOrder(Request $req, InvoiceCreator $ic, GeofenceTracker $geo) public function requestJobOrder(Request $req, InvoiceGeneratorInterface $ic, GeofenceTracker $geo)
{ {
// check required parameters and api key // check required parameters and api key
$required_params = [ $required_params = [
@ -979,7 +979,7 @@ class APIController extends Controller
$icrit->addEntry($batt, $trade_in, 1); $icrit->addEntry($batt, $trade_in, 1);
// send to invoice generator // send to invoice generator
$invoice = $ic->processCriteria($icrit); $invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice); $jo->setInvoice($invoice);
$em->persist($jo); $em->persist($jo);
@ -1026,7 +1026,7 @@ class APIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function getEstimate(Request $req, InvoiceCreator $ic) public function getEstimate(Request $req, InvoiceGeneratorInterface $ic)
{ {
// $this->debugRequest($req); // $this->debugRequest($req);
@ -1126,7 +1126,7 @@ class APIController extends Controller
$icrit->addEntry($batt, $trade_in, 1); $icrit->addEntry($batt, $trade_in, 1);
// send to invoice generator // send to invoice generator
$invoice = $ic->processCriteria($icrit); $invoice = $ic->generateInvoice($icrit);
// make invoice json data // make invoice json data
$data = [ $data = [

View file

@ -2,134 +2,54 @@
namespace App\Controller; namespace App\Controller;
use App\Ramcar\CustomerClassification;
use App\Ramcar\FuelType;
use App\Ramcar\VehicleStatusCondition;
use App\Ramcar\CrudException; use App\Ramcar\CrudException;
use App\Service\CustomerHandlerInterface;
use App\Entity\Customer; use App\Entity\Customer;
use App\Entity\CustomerVehicle;
use App\Entity\MobileNumber;
use App\Entity\Vehicle;
use App\Entity\VehicleManufacturer;
use App\Entity\Battery;
use App\Entity\BatteryManufacturer;
use Doctrine\ORM\Query; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Catalyst\MenuBundle\Annotation\Menu; use Catalyst\MenuBundle\Annotation\Menu;
use DateTime;
class CustomerController extends Controller class CustomerController extends Controller
{ {
/** /**
* @Menu(selected="customer_list") * @Menu(selected="customer_list")
*/ */
public function index() public function index(CustomerHandlerInterface $cust_handler)
{ {
$this->denyAccessUnlessGranted('customer.list', null, 'No access.'); $this->denyAccessUnlessGranted('customer.list', null, 'No access.');
return $this->render('customer/list.html.twig'); $params = $cust_handler->initializeCustomerIndexForm();
$template = $params['template'];
return $this->render($template);
} }
public function rows(Request $req) public function rows(Request $req, CustomerHandlerInterface $cust_handler)
{ {
$this->denyAccessUnlessGranted('customer.list', null, 'No access.'); $this->denyAccessUnlessGranted('customer.list', null, 'No access.');
// build query $params = $cust_handler->getCustomers($req);
$tqb = $this->getDoctrine()
->getRepository(Customer::class)
->createQueryBuilder('q');
$qb = $this->getDoctrine()
->getRepository(Customer::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $tqb->select('COUNT(q)');
// add filters to count query
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
// add filters to query
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.first_name', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
$meta = $params['meta'];
$rows = $params['rows'];
// process rows // process rows
$rows = []; foreach ($rows as $key => $data) {
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['title'] = $orow->getTitle();
$row['first_name'] = $orow->getFirstName();
$row['last_name'] = $orow->getLastName();
$row['customer_classification'] = CustomerClassification::getName($orow->getCustomerClassification());
$row['flag_mobile_app'] = $orow->hasMobileApp();
$row['app_mobile_number'] = $orow->hasMobileApp() && !empty($orow->getMobileSessions()) ? $orow->getMobileSessions()[0]->getPhoneNumber() : '';
$row['flag_active'] = $orow->isActive();
$row['flag_csat'] = $orow->isCSAT();
// TODO: properly add mobile numbers and plate numbers as searchable/sortable fields, use doctrine events
$row['mobile_numbers'] = implode("<br>", $orow->getMobileNumberList());
$row['plate_numbers'] = implode("<br>", $orow->getPlateNumberList());
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls // add crud urls
if ($this->isGranted('customer.update')) $cust_id = $rows[$key]['id'];
$row['meta']['update_url'] = $this->generateUrl('customer_update', ['id' => $row['id']]);
if ($this->isGranted('customer.delete'))
$row['meta']['delete_url'] = $this->generateUrl('customer_delete', ['id' => $row['id']]);
$rows[] = $row; if ($this->isGranted('customer.update'))
$rows[$key]['meta']['update_url'] = $this->generateUrl('customer_update', ['id' => $cust_id]);
if ($this->isGranted('customer.delete'))
$rows[$key]['meta']['delete_url'] = $this->generateUrl('customer_delete', ['id' => $cust_id]);
} }
// response // response
@ -139,311 +59,43 @@ class CustomerController extends Controller
]); ]);
} }
protected function fillDropdownParameters(&$params)
{
$em = $this->getDoctrine()->getManager();
$params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['classifications'] = CustomerClassification::getCollection();
$params['fuel_types'] = FuelType::getCollection();
$params['status_conditions'] = VehicleStatusCondition::getCollection();
$params['years'] = $this->generateYearOptions();
$params['batteries'] = $em->getRepository(Battery::class)->findAll();
}
/** /**
* @Menu(selected="customer_list") * @Menu(selected="customer_list")
*/ */
public function addForm() public function addForm(CustomerHandlerInterface $cust_handler)
{ {
$this->denyAccessUnlessGranted('customer.add', null, 'No access.'); $this->denyAccessUnlessGranted('customer.add', null, 'No access.');
$params['obj'] = new Customer(); $params = $cust_handler->initializeAddCustomerForm();
$params['mode'] = 'create';
// get dropdown parameters $template = $params['template'];
$this->fillDropdownParameters($params);
// response // response
return $this->render('customer/form.html.twig', $params); return $this->render($template, $params);
} }
protected function setObject($obj, $req) public function addSubmit(Request $req, CustomerHandlerInterface $cust_handler)
{
// set and save values
$obj->setTitle($req->request->get('title'))
->setFirstName($req->request->get('first_name'))
->setLastName($req->request->get('last_name'))
->setCustomerClassification($req->request->get('customer_classification'))
->setCustomerNotes($req->request->get('customer_notes'))
->setEmail($req->request->get('email'))
->setIsCSAT($req->request->get('flag_csat') ? true : false)
->setActive($req->request->get('flag_active') ? true : false);
// phone numbers
$obj->setPhoneMobile($req->request->get('phone_mobile'))
->setPhoneLandline($req->request->get('phone_landline'))
->setPhoneOffice($req->request->get('phone_office'))
->setPhoneFax($req->request->get('phone_fax'));
}
public function addSubmit(Request $req, ValidatorInterface $validator)
{ {
$this->denyAccessUnlessGranted('customer.add', null, 'No access.'); $this->denyAccessUnlessGranted('customer.add', null, 'No access.');
// create new row $result = $cust_handler->addCustomer($req);
$em = $this->getDoctrine()->getManager();
$row = new Customer();
$this->setObject($row, $req); if (isset($result['id']))
{
// initialize error lists $id = $result['id'];
$error_array = [];
$nerror_array = [];
$verror_array = [];
// error_log(print_r($req->request->all(), true));
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
if (!empty($vehicles)) {
foreach ($vehicles as $vehicle) {
// check if vehicle exists
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if (empty($vobj)) {
$verror_array[$vehicle->index]['vehicle'] = 'Invalid vehicle specified.';
} else {
$cust_vehicle = new CustomerVehicle();
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor($vehicle->color)
->setStatusCondition($vehicle->status_condition)
->setFuelType($vehicle->fuel_type)
->setActive($vehicle->flag_active)
->setCustomer($row);
// if specified, check if battery exists
if ($vehicle->battery) {
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if (empty($bobj)) {
$verror_array[$vehicle->index]['battery'] = 'Invalid battery specified.';
} else {
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
} else {
$cust_vehicle->setHasMotoliteBattery(false);
}
$verrors = $validator->validate($cust_vehicle);
// add errors to list
foreach ($verrors as $error) {
if (!isset($verror_array[$vehicle->index]))
$verror_array[$vehicle->index] = [];
$verror_array[$vehicle->index][$error->getPropertyPath()] = $error->getMessage();
}
// add to entity
if (!isset($verror_array[$vehicle->index])) {
$row->addVehicle($cust_vehicle);
}
}
}
}
// validate
$errors = $validator->validate($row);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array,
'nerrors' => $nerror_array,
'verrors' => $verror_array
], 422);
} else {
// validated! save the entity
$em->persist($row);
$em->flush();
// return successful response // return successful response
return $this->json([ return $this->json([
'success' => 'Changes have been saved!', 'success' => 'Changes have been saved!',
'id' => $row->getID() 'id' => $id
]); ]);
} }
}
/**
* @Menu(selected="customer_list")
*/
public function updateForm($id)
{
$this->denyAccessUnlessGranted('customer.update', null, 'No access.');
$params['mode'] = 'update';
// get row data
$em = $this->getDoctrine()->getManager();
$row = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($row))
throw $this->createNotFoundException('The item does not exist');
// get dropdown parameters
$this->fillDropdownParameters($params);
$params['obj'] = $row;
// response
return $this->render('customer/form.html.twig', $params);
}
protected function updateVehicles($em, Customer $cust, $vehicles)
{
$vehicle_ids = [];
foreach ($vehicles as $vehicle)
{
// check if customer vehicle exists
if (!empty($vehicle->id))
{
$cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($vehicle->id);
if ($cust_vehicle == null)
throw new CrudException("Could not find customer vehicle.");
}
// this is a new vehicle
else else
{ {
$cust_vehicle = new CustomerVehicle(); $error_array = $result['error_array'];
$cust_vehicle->setCustomer($cust); $nerror_array = $result['nerror_array'];
$cust->addVehicle($cust_vehicle); $verror_array = $result['verror_array'];
$em->persist($cust_vehicle);
}
// vehicle, because they could have changed vehicle type
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if ($vobj == null)
throw new CrudException("Could not find vehicle.");
// TODO: validate details
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor($vehicle->color)
->setStatusCondition($vehicle->status_condition)
->setFuelType($vehicle->fuel_type)
->setActive($vehicle->flag_active);
// if specified, check if battery exists
if ($vehicle->battery)
{
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if ($bobj == null)
throw new CrudException("Could not find battery.");
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
else
{
$cust_vehicle->setHasMotoliteBattery(false);
}
// add to list of vehicles to keep
$vehicle_ids[$cust_vehicle->getID()] = true;
}
// cleanup
// delete all vehicles not in list
$cvs = $cust->getVehicles();
foreach ($cvs as $cv)
{
if (!isset($vehicle_ids[$cv->getID()]))
{
$cust->removeVehicle($cv);
$em->remove($cv);
}
}
}
public function updateSubmit(Request $req, ValidatorInterface $validator, $id)
{
$this->denyAccessUnlessGranted('customer.update', null, 'No access.');
// get row data
$em = $this->getDoctrine()->getManager();
$cust = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($cust))
throw $this->createNotFoundException('The item does not exist');
$this->setObject($cust, $req);
// initialize error lists
$error_array = [];
$nerror_array = [];
$verror_array = [];
// TODO: validate mobile numbers
// TODO: validate vehicles
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
$this->updateVehicles($em, $cust, $vehicles);
// validate
$errors = $validator->validate($cust);
// add errors to list
foreach ($errors as $error)
{
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array))
{
// return validation failure response // return validation failure response
return $this->json([ return $this->json([
'success' => false, 'success' => false,
@ -452,34 +104,74 @@ class CustomerController extends Controller
'verrors' => $verror_array 'verrors' => $verror_array
], 422); ], 422);
} }
else }
/**
* @Menu(selected="customer_list")
*/
public function updateForm($id, CustomerHandlerInterface $cust_handler)
{ {
// validated! save the entity. do a persist anyway to save child entities $this->denyAccessUnlessGranted('customer.update', null, 'No access.');
$em->persist($cust);
$em->flush(); $params = $cust_handler->initializeUpdateCustomerForm($id);
$template = $params['template'];
// response
return $this->render($template, $params);
}
public function updateSubmit(Request $req, CustomerHandlerInterface $cust_handler, $id)
{
$this->denyAccessUnlessGranted('customer.update', null, 'No access.');
try
{
$result = $cust_handler->updateCustomer($req, $id);
}
catch (CrudException $e)
{
throw new CrudException($e->getMessage());
}
if (isset($result['id']))
{
$id = $result['id'];
// return successful response // return successful response
return $this->json([ return $this->json([
'success' => 'Changes have been saved!', 'success' => 'Changes have been saved!',
'id' => $cust->getID() 'id' => $id
]); ]);
} }
else
{
$error_array = $result['error_array'];
$nerror_array = $result['nerror_array'];
$verror_array = $result['verror_array'];
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array,
'nerrors' => $nerror_array,
'verrors' => $verror_array
], 422);
}
} }
public function destroy($id) public function destroy($id, CustomerHandlerInterface $cust_handler)
{ {
$this->denyAccessUnlessGranted('customer.delete', null, 'No access.'); $this->denyAccessUnlessGranted('customer.delete', null, 'No access.');
// get row data try
$em = $this->getDoctrine()->getManager(); {
$row = $em->getRepository(Customer::class)->find($id); $cust_handler->deleteCustomer($id);
}
if (empty($row)) catch (NotFoundHttpException $e)
throw $this->createNotFoundException('The item does not exist'); {
throw $this->createNotFoundException($e->getMessage());
// delete this row }
$em->remove($row);
$em->flush();
// response // response
$response = new Response(); $response = new Response();
@ -487,113 +179,17 @@ class CustomerController extends Controller
$response->send(); $response->send();
} }
protected function generateYearOptions() public function getCustomerVehicles(Request $req, CustomerHandlerInterface $cust_handler)
{
$start_year = 1950;
return range($start_year, date("Y") + 1);
}
public function getCustomerVehicles(Request $req)
{ {
if (!$this->isGranted('jo_in.list')) { if (!$this->isGranted('jo_in.list')) {
$exception = $this->createAccessDeniedException('No access.'); $exception = $this->createAccessDeniedException('No access.');
throw $exception; throw $exception;
} }
// get search term $results = $cust_handler->getCustomerVehicles($req);
$term = $req->query->get('search');
// get querybuilder $vehicles = $results['vehicles'];
$qb = $this->getDoctrine() $has_more_pages = $results['has_more_pages'];
->getRepository(CustomerVehicle::class)
->createQueryBuilder('q');
/*
// build expression now since we're reusing it
$vehicle_label = $qb->expr()->concat(
'q.plate_number',
$qb->expr()->literal(' - '),
'c.first_name',
$qb->expr()->literal(' '),
'c.last_name',
$qb->expr()->literal(' (+63'),
'c.phone_mobile',
$qb->expr()->literal(')')
);
*/
// count total records
$tquery = $qb->select('COUNT(q)');
// add filters to count query
if (!empty($term)) {
$tquery->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$tquery->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$tquery->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
$total = $tquery->getQuery()
->getSingleScalarResult();
// pagination vars
$page = $req->query->get('page') ?? 1;
$perpage = 20;
$offset = ($page - 1) * $perpage;
$pages = ceil($total / $perpage);
$has_more_pages = $page < $pages ? true : false;
// build main query
$query = $qb->select('q');
/*
->addSelect($vehicle_label . ' as vehicle_label')
->addSelect('c.first_name as cust_first_name')
->addSelect('c.last_name as cust_last_name');
*/
// add filters if needed
if (!empty($term)) {
$query->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$query->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$query->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
// get rows
$query_obj = $query->orderBy('q.plate_number', 'asc')
->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery();
// error_log($query_obj->getSql());
$obj_rows = $query_obj->getResult();
// build vehicles array
$vehicles = [];
// get country code from services.yaml
$country_code = $this->getParameter('country_code');
foreach ($obj_rows as $cv) {
$cust = $cv->getCustomer();
$vehicles[] = [
'id' => $cv->getID(),
'text' => $cv->getPlateNumber() . ' ' . $cust->getFirstName() . ' ' . $cust->getLastName() . ' (' . $country_code . $cust->getPhoneMobile() . ')',
];
}
// response // response
return $this->json([ return $this->json([
@ -605,85 +201,26 @@ class CustomerController extends Controller
]); ]);
} }
public function getCustomerVehicleInfo(Request $req) public function getCustomerVehicleInfo(Request $req, CustomerHandlerInterface $cust_handler)
{ {
$this->denyAccessUnlessGranted('jo_in.list', null, 'No access.'); $this->denyAccessUnlessGranted('jo_in.list', null, 'No access.');
// get id $result = $cust_handler->getCustomerVehicleInfo($req);
$id = $req->query->get('id');
// get row data if ($result == null)
$em = $this->getDoctrine()->getManager(); {
$obj = $em->getRepository(CustomerVehicle::class)->find($id);
// make sure this row exists
if (empty($obj)) {
return $this->json([ return $this->json([
'success' => false, 'success' => false,
'error' => 'The item does not exist' 'error' => 'The item does not exist'
]); ]);
} }
else
$customer = $obj->getCustomer(); {
$vehicle = $obj->getVehicle();
$battery = $obj->getCurrentBattery();
// build response
$row = [
'customer' => [
'id' => $customer->getID(),
'first_name' => $customer->getFirstName(),
'last_name' => $customer->getLastName(),
'customer_notes' => $customer->getCustomerNotes(),
'phone_mobile' => $customer->getPhoneMobile(),
'phone_landline' => $customer->getPhoneLandline(),
'phone_office' => $customer->getPhoneOffice(),
'phone_fax' => $customer->getPhoneFax(),
],
'vehicle' => [
'id' => $vehicle->getID(),
'mfg_name' => $vehicle->getManufacturer()->getName(),
'make' => $vehicle->getMake(),
'model_year_from' => $vehicle->getModelYearFrom(),
'model_year_to' => $vehicle->getModelYearTo(),
'model_year' => $obj->getModelYear(),
'color' => $obj->getColor(),
'plate_number' => $obj->getPlateNumber(),
//'fuel_type' => $obj->getFuelType(),
//'status_condition' => $obj->getStatusCondition(),
]
];
if (!empty($battery)) {
$row['battery'] = [
'id' => $battery->getID(),
'mfg_name' => $battery->getManufacturer()->getName(),
'model_name' => $battery->getModel()->getName(),
'size_name' => $battery->getSize()->getName(),
'prod_code' => $battery->getProductCode(),
'warranty_code' => $obj->getWarrantyCode(),
'warranty_expiration' => $obj->getWarrantyExpiration() ? $obj->getWarrantyExpiration()->format("d M Y") : "",
'has_motolite_battery' => $obj->hasMotoliteBattery(),
'is_active' => $obj->isActive()
];
}
// response // response
return $this->json([ return $this->json([
'success' => true, 'success' => true,
'data' => $row 'data' => $result
]); ]);
} }
// check if datatable filter is present and append to query
protected function setQueryFilters($datatable, &$query) {
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->join('q.vehicles', 'cv')
->where('q.first_name LIKE :filter')
->orWhere('q.last_name LIKE :filter')
->orWhere('q.customer_classification LIKE :filter')
->orWhere('cv.plate_number LIKE :filter')
->setParameter('filter', $datatable['query']['data-rows-search'] . '%');
}
} }
} }

View file

@ -5,13 +5,122 @@ namespace App\Controller;
use Catalyst\MenuBundle\Annotation\Menu; use Catalyst\MenuBundle\Annotation\Menu;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\RiderTracker;
use App\Service\GISManagerInterface;
use App\Service\JobOrderCache;
use App\Service\RiderCache;
use App\Entity\Rider;
class HomeController extends Controller class HomeController extends Controller
{ {
/** /**
* @Menu(selected="home") * @Menu(selected="home")
*/ */
public function index() public function index(
EntityManagerInterface $em,
RiderTracker $rider_tracker,
GISManagerInterface $gis_manager
)
{ {
return $this->render('home.html.twig'); // get map
$params['map_js_file'] = $gis_manager->getJSInitFile();
return $this->render('home.html.twig', $params);
}
public function getMapLocations(JobOrderCache $jo_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 = [];
foreach ($riders as $rider)
{
// get location for each rider
$rider_id = $rider->getID();
$coordinates = $rider_tracker->getRiderLocation($rider_id);
$lat = $coordinates->getLatitude();
$long = $coordinates->getLongitude();
$jo = $rider->getActiveJobOrder();
if ($jo == null)
{
$has_jo = false;
$clat = 0;
$clong = 0;
$jo_data = [];
}
else
{
$has_jo = true;
$cust_loc = $jo->getCoordinates();
$clat = $cust_loc->getLatitude();
$clong = $cust_loc->getLongitude();
$jo_id = $jo->getID();
$jo_data = [
'id' => $jo_id,
'status' => $jo->getStatusText(),
'stype' => $jo->getServiceTypeName(),
'url' => $this->generateUrl('jo_all_form', ['id' => $jo_id]),
'plate' => $jo->getCustomerVehicle()->getPlateNumber(),
'cname' => $jo->getCustomer()->getNameDisplay(),
];
}
// use rider map label as key
$rider_map_label = $rider->getMapLabel();
$locations[$rider_id] = [
'label' => $rider->getMapLabel(),
'loc' => [$lat, $long],
'has_jo' => $has_jo,
'cust_loc' => [$clat, $clong],
'jo' => $jo_data,
];
}
*/
return $this->json([
'jos' => $active_jos,
'riders' => $riders,
]);
} }
} }

View file

@ -17,6 +17,9 @@ use DateTime;
use Catalyst\MenuBundle\Annotation\Menu; use Catalyst\MenuBundle\Annotation\Menu;
use App\Service\MapTools;
use App\Service\RiderTracker;
class HubController extends Controller class HubController extends Controller
{ {
/** /**
@ -287,4 +290,87 @@ class HubController extends Controller
$response->setStatusCode(Response::HTTP_OK); $response->setStatusCode(Response::HTTP_OK);
$response->send(); $response->send();
} }
public function nearest(MapTools $map_tools, Request $req)
{
// get lat / long
$lat = $req->query->get('lat');
$long = $req->query->get('long');
// get nearest hubs according to position
$point = new Point($long, $lat);
$result = $map_tools->getClosestHubs($point, 10, date("H:i:s"));
$hubs = [];
foreach ($result as $hub_res)
{
$hub = $hub_res['hub'];
$coords = $hub->getCoordinates();
$hubs[] = [
'id' => $hub->getID(),
'long' => $coords->getLongitude(),
'lat' => $coords->getLatitude(),
'label' => $hub->getFullName(),
'name' => $hub->getName(),
'branch' => $hub->getBranch(),
'cnum' => $hub->getContactNumbers(),
'distance' => $hub_res['distance'],
];
}
return $this->json([
'hubs' => $hubs,
]);
}
public function getHubRiders(Request $req, RiderTracker $rider_tracker)
{
// get hub id
$hub_id = $req->query->get('id');
// get hub
$em = $this->getDoctrine()->getManager();
$hub = $em->getRepository(Hub::class)->find($hub_id);
// make sure this row exists
if (empty($hub))
throw $this->createNotFoundException('The item does not exist');
//TODO: get available riders sort by proximity, show 10
$available_riders = $hub->getAvailableRiders();
$riders = [];
// TODO: remove this later when we don't get all available riders
$riders_limit = 5;
$num_riders = 0;
foreach ($available_riders as $rider)
{
if ($num_riders > $riders_limit)
break;
// get location for each rider
$rider_id = $rider->getID();
$coordinates = $rider_tracker->getRiderLocation($rider_id);
$lat = $coordinates->getLatitude();
$long = $coordinates->getLongitude();
$riders[] = [
'id' => $rider->getID(),
'first_name' => $rider->getFirstName(),
'last_name' => $rider->getLastName(),
'contact_num' => $rider->getContactNumber(),
'plate_num' => $rider->getPlateNumber(),
'location' => [$lat, $long],
];
$num_riders++;
}
return $this->json([
'riders' => $riders,
]);
}
} }

File diff suppressed because it is too large Load diff

View file

@ -16,18 +16,22 @@ use CrEOF\Spatial\PHP\Types\Geometry\Point;
use App\Ramcar\APIResult; use App\Ramcar\APIResult;
use App\Ramcar\JOStatus; use App\Ramcar\JOStatus;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
use App\Ramcar\CMBServiceType;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\WarrantyClass; use App\Ramcar\WarrantyClass;
use App\Ramcar\APIRiderStatus; use App\Ramcar\APIRiderStatus;
use App\Ramcar\TransactionOrigin; use App\Ramcar\TransactionOrigin;
use App\Ramcar\CMBTradeInType;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\InvoiceStatus; use App\Ramcar\InvoiceStatus;
use App\Ramcar\ModeOfPayment; use App\Ramcar\ModeOfPayment;
use App\Ramcar\JOEventType; use App\Ramcar\JOEventType;
use App\Service\InvoiceCreator; use App\Service\InvoiceGeneratorInterface;
use App\Service\MQTTClient; use App\Service\MQTTClient;
use App\Service\WarrantyHandler; use App\Service\WarrantyHandler;
use App\Service\RedisClientProvider;
use App\Service\RiderCache;
use App\Entity\RiderSession; use App\Entity\RiderSession;
use App\Entity\Customer; use App\Entity\Customer;
@ -43,11 +47,13 @@ use App\Entity\RiderRating;
use App\Entity\Rider; use App\Entity\Rider;
use App\Entity\User; use App\Entity\User;
use App\Entity\JOEvent; use App\Entity\JOEvent;
use App\Entity\Warranty;
use DateTime; use DateTime;
use DateInterval;
// Rider API controller // Rider API controller
// TODO: Need to refactor this into a service
class RAPIController extends Controller class RAPIController extends Controller
{ {
protected $session; protected $session;
@ -136,7 +142,7 @@ class RAPIController extends Controller
return $res; return $res;
} }
public function register(Request $req) public function register(Request $req, RedisClientProvider $redis)
{ {
$res = new APIResult(); $res = new APIResult();
@ -179,6 +185,12 @@ class RAPIController extends Controller
// save // save
$em->persist($sess); $em->persist($sess);
$em->flush(); $em->flush();
// create redis entry for the session
$redis_client = $redis->getRedisClient();
$redis_key = 'rider.id.' . $sess->getID();
error_log('redis_key: ' . $redis_key);
$redis_client->set($redis_key, '');
} }
catch (DBALException $e) catch (DBALException $e)
{ {
@ -202,7 +214,7 @@ class RAPIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function login(Request $req, EncoderFactoryInterface $ef) public function login(Request $req, EncoderFactoryInterface $ef, RedisClientProvider $redis, RiderCache $rcache)
{ {
$required_params = [ $required_params = [
'user', 'user',
@ -244,10 +256,22 @@ class RAPIController extends Controller
$rider->setAvailable(true); $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 // TODO: log rider logging in
$em->flush(); $em->flush();
// update redis rider.id.<session id> with the rider id
$redis_client = $redis->getRedisClient();
$redis_key = 'rider.id.' . $this->session->getID();
$rider_id = $rider->getID();
$redis_client->set($redis_key, $rider_id);
$hub = $rider->getHub(); $hub = $rider->getHub();
if ($hub == null) if ($hub == null)
$hub_data = null; $hub_data = null;
@ -266,7 +290,8 @@ class RAPIController extends Controller
// data // data
$data = [ $data = [
'hub' => $hub_data 'hub' => $hub_data,
'rider_id' => $rider_id,
]; ];
$res->setData($data); $res->setData($data);
@ -274,7 +299,7 @@ class RAPIController extends Controller
return $res->getReturnResponse(); return $res->getReturnResponse();
} }
public function logout(Request $req) public function logout(Request $req, RiderCache $rcache)
{ {
$required_params = []; $required_params = [];
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
@ -286,6 +311,9 @@ class RAPIController extends Controller
$rider = $this->session->getRider(); $rider = $this->session->getRider();
$rider->setAvailable(false); $rider->setAvailable(false);
// remove from cache
$rcache->removeActiveRider($rider->getID());
// remove rider from session // remove rider from session
$this->session->setRider(null); $this->session->setRider(null);
@ -621,27 +649,30 @@ class RAPIController extends Controller
// save to customer vehicle battery record // save to customer vehicle battery record
// TODO: this has to move to JOHandler // TODO: this has to move to JOHandler
$this->updateVehicleBattery($obj); $this->updateVehicleBattery($jo);
$em->flush();
// create warranty // create warranty
if ($obj->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServiceType() == CMBServiceType::BATTERY_REPLACEMENT_NEW))
{ {
$serial = null; $serial = null;
$warranty_class = $obj->getWarrantyClass(); $warranty_class = $jo->getWarrantyClass();
$first_name = $obj->getCustomer()->getFirstName(); $first_name = $jo->getCustomer()->getFirstName();
$last_name = $obj->getCustomer()->getLastName(); $last_name = $jo->getCustomer()->getLastName();
$mobile_number = $obj->getCustomer()->getPhoneMobile(); $mobile_number = $jo->getCustomer()->getPhoneMobile();
// check if date fulfilled is null // check if date fulfilled is null
if ($obj->getDateFulfill() == null) if ($jo->getDateFulfill() == null)
$date_purchase = $obj->getDateCreate(); $date_purchase = $jo->getDateCreate();
else else
$date_purchase = $obj->getDateFulfill(); $date_purchase = $jo->getDateFulfill();
$plate_number = $wh->cleanPlateNumber($obj->getCustomerVehicle()->getPlateNumber()); $plate_number = $wh->cleanPlateNumber($jo->getCustomerVehicle()->getPlateNumber());
$batt_list = array(); $batt_list = array();
$invoice = $obj->getInvoice(); $invoice = $jo->getInvoice();
if (!empty($invoice)) if (!empty($invoice))
{ {
// get battery // get battery
@ -659,8 +690,6 @@ class RAPIController extends Controller
$wh->createWarranty($serial, $plate_number, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase, $warranty_class); $wh->createWarranty($serial, $plate_number, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase, $warranty_class);
} }
$em->flush();
// send mqtt event (fulfilled) // send mqtt event (fulfilled)
$rider = $this->session->getRider(); $rider = $this->session->getRider();
$image_url = $req->getScheme() . '://' . $req->getHttpHost() . $req->getBasePath() . '/assets/images/user.gif'; $image_url = $req->getScheme() . '://' . $req->getHttpHost() . $req->getBasePath() . '/assets/images/user.gif';
@ -780,7 +809,7 @@ class RAPIController extends Controller
error_log(print_r($all, true)); error_log(print_r($all, true));
} }
public function changeService(Request $req, InvoiceCreator $ic) public function changeService(Request $req, InvoiceGeneratorInterface $ic)
{ {
$this->debugRequest($req); $this->debugRequest($req);
@ -793,7 +822,8 @@ class RAPIController extends Controller
// check service type // check service type
$stype_id = $req->request->get('stype_id'); $stype_id = $req->request->get('stype_id');
if (!ServiceType::validate($stype_id)) if ((!CMBServiceType::validate($stype_id)) ||
(!ServiceType::validate($stype_id)))
{ {
$res->setError(true) $res->setError(true)
->setErrorMessage('Invalid service type - ' . $stype_id); ->setErrorMessage('Invalid service type - ' . $stype_id);
@ -855,7 +885,8 @@ class RAPIController extends Controller
// check trade in // check trade in
$trade_in = $req->request->get('trade_in'); $trade_in = $req->request->get('trade_in');
if (!TradeInType::validate($trade_in)) if ((!CMBTradeInType::validate($trade_in)) ||
(!TradeInType::validate($trade_in)))
$trade_in = null; $trade_in = null;
// check mode of payment // check mode of payment
@ -880,8 +911,7 @@ class RAPIController extends Controller
error_log('adding entry for battery - ' . $battery->getID()); error_log('adding entry for battery - ' . $battery->getID());
} }
$invoice = $ic->processCriteria($crit); $invoice = $ic->generateInvoice($crit);
$invoice->setStatus(InvoiceStatus::DRAFT);
// remove previous invoice // remove previous invoice
$old_invoice = $jo->getInvoice(); $old_invoice = $jo->getInvoice();
@ -913,7 +943,8 @@ class RAPIController extends Controller
protected function updateVehicleBattery(JobOrder $jo) protected function updateVehicleBattery(JobOrder $jo)
{ {
// check if new battery // check if new battery
if ($jo->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW) if (($jo->getServiceType() != ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServiceType() != CMBServiceType::BATTERY_REPLACEMENT_NEW))
return; return;
// customer vehicle // customer vehicle
@ -945,6 +976,7 @@ class RAPIController extends Controller
return; return;
// warranty expiration // warranty expiration
$warr_months = 0;
$warr = $jo->getWarrantyClass(); $warr = $jo->getWarrantyClass();
if ($warr == WarrantyClass::WTY_PRIVATE) if ($warr == WarrantyClass::WTY_PRIVATE)
$warr_months = $battery->getWarrantyPrivate(); $warr_months = $battery->getWarrantyPrivate();
@ -961,4 +993,5 @@ class RAPIController extends Controller
->setHasMotoliteBattery(true) ->setHasMotoliteBattery(true)
->setWarrantyExpiration($warr_date); ->setWarrantyExpiration($warr_date);
} }
} }

View file

@ -10,6 +10,7 @@ use App\Entity\User;
use App\Service\FileUploader; use App\Service\FileUploader;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
@ -500,4 +501,13 @@ class RiderController extends Controller
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%'); ->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
} }
} }
public function popupInfo(EntityManagerInterface $em, $id)
{
$rider = $em->getRepository(Rider::class)->find($id);
if ($rider == null)
return new Response('No rider data');
return $this->render('rider/popup.html.twig', [ 'rider' => $rider ]);
}
} }

View file

@ -60,14 +60,13 @@ class Battery
// product code // product code
/** /**
* @ORM\Column(type="string", length=80) * @ORM\Column(type="string", length=80, nullable=true)
* @Assert\NotBlank()
*/ */
protected $prod_code; protected $prod_code;
// sap code // sap code
/** /**
* @ORM\Column(type="string", length=80) * @ORM\Column(type="string", length=80, nullable=true)
*/ */
protected $sap_code; protected $sap_code;

View file

@ -231,6 +231,11 @@ class Customer
return $this->last_name; return $this->last_name;
} }
public function getNameDisplay()
{
return $this->first_name . ' ' . $this->last_name;
}
public function setCustomerClassification($customer_classification) public function setCustomerClassification($customer_classification)
{ {
$this->customer_classification = $customer_classification; $this->customer_classification = $customer_classification;

View file

@ -67,14 +67,12 @@ class CustomerVehicle
// vehicle status (new / second-hand) // vehicle status (new / second-hand)
/** /**
* @ORM\Column(type="string", length=15) * @ORM\Column(type="string", length=15)
* @Assert\NotBlank()
*/ */
protected $status_condition; protected $status_condition;
// fuel type - diesel, gas // fuel type - diesel, gas
/** /**
* @ORM\Column(type="string", length=15) * @ORM\Column(type="string", length=15)
* @Assert\NotBlank()
*/ */
protected $fuel_type; protected $fuel_type;

View file

@ -319,4 +319,10 @@ class Rider
{ {
return $this->sessions; return $this->sessions;
} }
public function getMapLabel()
{
$map_label = $this->first_name .' ' . $this->last_name;
return $map_label;
}
} }

View file

@ -0,0 +1,105 @@
<?php
namespace App\EventListener;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use App\Service\JobOrderCache;
use App\Ramcar\JOStatus;
use App\Entity\JobOrder;
class JobOrderActiveCacheListener
{
protected $key;
protected $mqtt;
public function __construct(JobOrderCache $jo_cache, $mqtt)
{
$this->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
);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Ramcar;
class CMBModeOfPayment extends NameValue
{
const CASH = 'cash';
const CREDIT_CARD = 'credit_card';
const BANK_TRANSFER = 'bank_transfer';
const COLLECTION = [
'cash' => 'Cash',
'credit_card' => 'Credit Card',
'bank_transfer' => 'Bank Transfer',
];
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Ramcar;
class CMBServiceType extends NameValue
{
const BATTERY_REPLACEMENT_NEW = 'battery_new';
const BATTERY_REPLACEMENT_WARRANTY = 'battery_warranty';
const JUMPSTART = 'jumpstart';
const COLLECTION = [
'battery_new' => 'Battery Sales',
'battery_warranty' => 'Under Warranty',
'jumpstart' => 'Jumpstart',
];
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Ramcar;
class CMBTradeInType extends NameValue
{
const YES = 'yes';
const COLLECTION = [
'yes' => 'Yes',
];
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Ramcar;
class CMBWarrantyClass extends NameValue
{
const WTY_PASSENGER = 'passenger';
const WTY_COMMERCIAL = 'commercial';
const COLLECTION = [
'passenger' => 'Passenger',
'commercial' => 'Commercial',
];
}

View file

@ -0,0 +1,698 @@
<?php
namespace App\Service\CustomerHandler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Ramcar\CustomerClassification;
use App\Ramcar\CrudException;
use App\Service\CustomerHandlerInterface;
use App\Entity\Customer;
use App\Entity\CustomerVehicle;
use App\Entity\Vehicle;
use App\Entity\Battery;
use App\Entity\VehicleManufacturer;
use App\Entity\BatteryManufacturer;
use DateTime;
class CMBCustomerHandler implements CustomerHandlerInterface
{
protected $em;
protected $validator;
protected $country_code;
protected $template_hash;
public function __construct(EntityManagerInterface $em, ValidatorInterface $validator,
string $country_code)
{
$this->em = $em;
$this->validator = $validator;
$this->country_code = $country_code;
$this->loadTemplates();
}
// initialize form to display customer list
public function initializeCustomerIndexForm()
{
$params['template'] = $this->getTwigTemplate('cust_list');
return $params;
}
// get customers
public function getCustomers(Request $req)
{
// build query
$tqb = $this->em->getRepository(Customer::class)
->createQueryBuilder('q');
$qb = $this->em->getRepository(Customer::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $tqb->select('COUNT(q)');
// add filters to count query
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
// add filters to query
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.first_name', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['title'] = $orow->getTitle();
$row['first_name'] = $orow->getFirstName();
$row['last_name'] = $orow->getLastName();
$row['customer_classification'] = CustomerClassification::getName($orow->getCustomerClassification());
$row['flag_mobile_app'] = $orow->hasMobileApp();
$row['app_mobile_number'] = $orow->hasMobileApp() && !empty($orow->getMobileSessions()) ? $orow->getMobileSessions()[0]->getPhoneNumber() : '';
$row['flag_active'] = $orow->isActive();
$row['flag_csat'] = $orow->isCSAT();
// TODO: properly add mobile numbers and plate numbers as searchable/sortable fields, use doctrine events
// prepend the country code before each mobile number
$mobile_number_list = [];
$mobile_numbers = $orow->getMobileNumberList();
foreach ($mobile_numbers as $mobile_number)
{
$mobile_number_list[] = $this->country_code . $mobile_number;
}
$row['mobile_numbers'] = implode("<br>", $mobile_number_list);
$row['plate_numbers'] = implode("<br>", $orow->getPlateNumberList());
$rows[] = $row;
}
$params['meta'] = $meta;
$params['rows'] = $rows;
return $params;
}
// initialize add customer form
public function initializeAddCustomerForm()
{
$params['obj'] = new Customer();
$params['mode'] = 'create';
// get dropdown parameters
$this->fillDropdownParameters($params);
// get template to display
$params['template'] = $this->getTwigTemplate('cust_add_form');
// return params
return $params;
}
// add new customer and customer vehicle, if any
public function addCustomer(Request $req)
{
// create new row
$em = $this->em;
$row = new Customer();
$this->setObject($row, $req);
// initialize error lists
$error_array = [];
$nerror_array = [];
$verror_array = [];
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
if (!empty($vehicles))
{
foreach ($vehicles as $vehicle)
{
// check if vehicle exists
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if (empty($vobj))
{
$verror_array[$vehicle->index]['vehicle'] = 'Invalid vehicle specified.';
}
else
{
$cust_vehicle = new CustomerVehicle();
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor('')
->setStatusCondition('')
->setFuelType('')
->setActive($vehicle->flag_active)
->setCustomer($row);
// if specified, check if battery exists
if ($vehicle->battery)
{
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if (empty($bobj))
{
$verror_array[$vehicle->index]['battery'] = 'Invalid battery specified.';
}
else
{
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
}
else
{
$cust_vehicle->setHasMotoliteBattery(false);
}
$verrors = $this->validator->validate($cust_vehicle);
// add errors to list
foreach ($verrors as $error)
{
if (!isset($verror_array[$vehicle->index]))
$verror_array[$vehicle->index] = [];
$verror_array[$vehicle->index][$error->getPropertyPath()] = $error->getMessage();
}
// add to entity
if (!isset($verror_array[$vehicle->index]))
{
$row->addVehicle($cust_vehicle);
}
}
}
}
// validate
$errors = $this->validator->validate($row);
// add errors to list
foreach ($errors as $error)
{
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
$result = [];
// check if any errors were found
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array))
{
// return all error_arrays
$result = [
'error_array' => $error_array,
'nerror_array' => $nerror_array,
'verror_array' => $verror_array,
];
}
else
{
// validated! save the entity
$em->persist($row);
$em->flush();
$result = [
'id' => $row->getID(),
];
}
return $result;
}
// initialize update customer form
public function initializeUpdateCustomerForm($id)
{
$params['mode'] = 'update';
// get row data
$em = $this->em;
$row = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($row))
throw new NotFoundHttpException('The item does not exist');
// get dropdown parameters
$this->fillDropdownParameters($params);
// get template to display
$params['template'] = $this->getTwigTemplate('cust_update_form');
$params['obj'] = $row;
return $params;
}
// update customer and customer vehicle
public function updateCustomer(Request $req, $id)
{
// get row data
$em = $this->em;
$cust = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($cust))
throw $this->createNotFoundException('The item does not exist');
$this->setObject($cust, $req);
// initialize error lists
$error_array = [];
$nerror_array = [];
$verror_array = [];
// TODO: validate mobile numbers
// TODO: validate vehicles
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
try
{
$this->updateVehicles($em, $cust, $vehicles);
}
catch (CrudException $e)
{
throw new CrudException($e->getMessage());
}
// validate
$errors = $this->validator->validate($cust);
// add errors to list
foreach ($errors as $error)
{
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
$result = [];
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array))
{
// return all error_arrays
$result = [
'error_array' => $error_array,
'nerror_array' => $nerror_array,
'verror_array' => $verror_array,
];
}
else
{
// validated! save the entity. do a persist anyway to save child entities
$em->persist($cust);
$em->flush();
$result = [
'id' => $cust->getID(),
];
}
return $result;
}
// delete customer
public function deleteCustomer(int $id)
{
// get row data
$em = $this->em;
$row = $em->getRepository(Customer::class)->find($id);
if (empty($row))
throw new NotFoundHttpException('The item does not exist');
// delete this row
$em->remove($row);
$em->flush();
}
// get customer vehicles
public function getCustomerVehicles(Request $req)
{
// get search term
$term = $req->query->get('search');
// get querybuilder
$qb = $this->em
->getRepository(CustomerVehicle::class)
->createQueryBuilder('q');
/*
// build expression now since we're reusing it
$vehicle_label = $qb->expr()->concat(
'q.plate_number',
$qb->expr()->literal(' - '),
'c.first_name',
$qb->expr()->literal(' '),
'c.last_name',
$qb->expr()->literal(' (+63'),
'c.phone_mobile',
$qb->expr()->literal(')')
);
*/
// count total records
$tquery = $qb->select('COUNT(q)');
// add filters to count query
if (!empty($term)) {
$tquery->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$tquery->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$tquery->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
$total = $tquery->getQuery()
->getSingleScalarResult();
// pagination vars
$page = $req->query->get('page') ?? 1;
$perpage = 20;
$offset = ($page - 1) * $perpage;
$pages = ceil($total / $perpage);
$has_more_pages = $page < $pages ? true : false;
// build main query
$query = $qb->select('q');
/*
->addSelect($vehicle_label . ' as vehicle_label')
->addSelect('c.first_name as cust_first_name')
->addSelect('c.last_name as cust_last_name');
*/
// add filters if needed
if (!empty($term)) {
$query->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$query->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$query->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
// get rows
$query_obj = $query->orderBy('q.plate_number', 'asc')
->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery();
// error_log($query_obj->getSql());
$obj_rows = $query_obj->getResult();
// build vehicles array
$vehicles = [];
foreach ($obj_rows as $cv) {
$cust = $cv->getCustomer();
$vehicles[] = [
'id' => $cv->getID(),
'text' => $cv->getPlateNumber() . ' ' . $cust->getFirstName() . ' ' . $cust->getLastName() . ' ('. $this->country_code . $cust->getPhoneMobile() . ')',
];
}
$results = [
'vehicles' => $vehicles,
'has_more_pages' => $has_more_pages,
];
return $results;
}
// get customer vehicle info
public function getCustomerVehicleInfo(Request $req)
{
// get id
$id = $req->query->get('id');
// get row data
$em = $this->em;
$obj = $em->getRepository(CustomerVehicle::class)->find($id);
// make sure this row exists
if (empty($obj)) {
return null;
}
$customer = $obj->getCustomer();
$vehicle = $obj->getVehicle();
$battery = $obj->getCurrentBattery();
// build response
$row = [
'customer' => [
'id' => $customer->getID(),
'first_name' => $customer->getFirstName(),
'last_name' => $customer->getLastName(),
'customer_notes' => $customer->getCustomerNotes(),
'phone_mobile' => $customer->getPhoneMobile(),
'phone_landline' => $customer->getPhoneLandline(),
'phone_office' => $customer->getPhoneOffice(),
'phone_fax' => $customer->getPhoneFax(),
],
'vehicle' => [
'id' => $vehicle->getID(),
'mfg_name' => $vehicle->getManufacturer()->getName(),
'make' => $vehicle->getMake(),
'model_year_from' => $vehicle->getModelYearFrom(),
'model_year_to' => $vehicle->getModelYearTo(),
'model_year' => $obj->getModelYear(),
'color' => $obj->getColor(),
'plate_number' => $obj->getPlateNumber(),
]
];
if (!empty($battery)) {
$row['battery'] = [
'id' => $battery->getID(),
'mfg_name' => $battery->getManufacturer()->getName(),
'model_name' => $battery->getModel()->getName(),
'size_name' => $battery->getSize()->getName(),
'prod_code' => $battery->getProductCode(),
'warranty_code' => $obj->getWarrantyCode(),
'warranty_expiration' => $obj->getWarrantyExpiration() ? $obj->getWarrantyExpiration()->format("d M Y") : "",
'has_motolite_battery' => $obj->hasMotoliteBattery(),
'is_active' => $obj->isActive()
];
}
return $row;
}
protected function getTwigTemplate($id)
{
if (isset($this->template_hash[$id]))
{
return $this->template_hash[$id];
}
return null;
}
protected function setObject($obj, $req)
{
// set and save values
$obj->setTitle($req->request->get('title'))
->setFirstName($req->request->get('first_name'))
->setLastName($req->request->get('last_name'))
->setCustomerClassification($req->request->get('customer_classification'))
->setCustomerNotes($req->request->get('customer_notes'))
->setEmail($req->request->get('email'))
->setIsCSAT($req->request->get('flag_csat') ? true : false)
->setActive($req->request->get('flag_active') ? true : false);
// phone numbers
$obj->setPhoneMobile($req->request->get('phone_mobile'))
->setPhoneLandline($req->request->get('phone_landline'))
->setPhoneOffice($req->request->get('phone_office'))
->setPhoneFax($req->request->get('phone_fax'));
}
protected function fillDropdownParameters(&$params)
{
$em = $this->em;
$params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['classifications'] = CustomerClassification::getCollection();
$params['years'] = $this->generateYearOptions();
$params['batteries'] = $em->getRepository(Battery::class)->findAll();
}
protected function generateYearOptions()
{
$start_year = 1950;
return range($start_year, date("Y") + 1);
}
protected function loadTemplates()
{
$this->template_hash = [];
// add all twig templates for customer to hash
$this->template_hash['cust_add_form'] = 'customer/cmb.form.html.twig';
$this->template_hash['cust_update_form'] = 'customer/cmb.form.html.twig';
$this->template_hash['cust_list'] = 'customer/list.html.twig';
}
protected function updateVehicles($em, Customer $cust, $vehicles)
{
$vehicle_ids = [];
foreach ($vehicles as $vehicle)
{
// check if customer vehicle exists
if (!empty($vehicle->id))
{
$cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($vehicle->id);
if ($cust_vehicle == null)
throw new CrudException("Could not find customer vehicle.");
}
// this is a new vehicle
else
{
$cust_vehicle = new CustomerVehicle();
$cust_vehicle->setCustomer($cust);
$cust->addVehicle($cust_vehicle);
$em->persist($cust_vehicle);
}
// vehicle, because they could have changed vehicle type
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if ($vobj == null)
throw new CrudException("Could not find vehicle.");
// TODO: validate details
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor('')
->setStatusCondition('')
->setFuelType('')
->setActive($vehicle->flag_active);
// if specified, check if battery exists
if ($vehicle->battery)
{
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if ($bobj == null)
throw new CrudException("Could not find battery.");
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
else
{
$cust_vehicle->setHasMotoliteBattery(false);
}
// add to list of vehicles to keep
$vehicle_ids[$cust_vehicle->getID()] = true;
}
// cleanup
// delete all vehicles not in list
$cvs = $cust->getVehicles();
foreach ($cvs as $cv)
{
if (!isset($vehicle_ids[$cv->getID()]))
{
$cust->removeVehicle($cv);
$em->remove($cv);
}
}
}
// check if datatable filter is present and append to query
protected function setQueryFilters($datatable, &$query) {
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->join('q.vehicles', 'cv')
->where('q.first_name LIKE :filter')
->orWhere('q.last_name LIKE :filter')
->orWhere('q.customer_classification LIKE :filter')
->orWhere('cv.plate_number LIKE :filter')
->setParameter('filter', $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -0,0 +1,700 @@
<?php
namespace App\Service\CustomerHandler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Service\CustomerHandlerInterface;
use App\Ramcar\CustomerClassification;
use App\Ramcar\FuelType;
use App\Ramcar\VehicleStatusCondition;
use App\Ramcar\CrudException;
use App\Entity\Customer;
use App\Entity\CustomerVehicle;
use App\Entity\Vehicle;
use App\Entity\Battery;
use App\Entity\VehicleManufacturer;
use App\Entity\BatteryManufacturer;
use DateTime;
class ResqCustomerHandler implements CustomerHandlerInterface
{
protected $em;
protected $validator;
protected $country_code;
protected $template_hash;
public function __construct(EntityManagerInterface $em, ValidatorInterface $validator,
string $country_code)
{
$this->em = $em;
$this->validator = $validator;
$this->country_code = $country_code;
$this->loadTemplates();
}
// initialize form to display customer list
public function initializeCustomerIndexForm()
{
$params['template'] = $this->getTwigTemplate('cust_list');
return $params;
}
// get customers
public function getCustomers(Request $req)
{
// build query
$tqb = $this->em->getRepository(Customer::class)
->createQueryBuilder('q');
$qb = $this->em->getRepository(Customer::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $tqb->select('COUNT(q)');
// add filters to count query
$this->setQueryFilters($datatable, $tquery);
$total = $tquery->getQuery()
->getSingleScalarResult();
// get current page number
$page = $datatable['pagination']['page'] ?? 1;
$perpage = $datatable['pagination']['perpage'];
$offset = ($page - 1) * $perpage;
// add metadata
$meta = [
'page' => $page,
'perpage' => $perpage,
'pages' => ceil($total / $perpage),
'total' => $total,
'sort' => 'asc',
'field' => 'id'
];
// build query
$query = $qb->select('q');
// add filters to query
$this->setQueryFilters($datatable, $query);
// check if sorting is present, otherwise use default
if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) {
$order = $datatable['sort']['sort'] ?? 'asc';
$query->orderBy('q.' . $datatable['sort']['field'], $order);
} else {
$query->orderBy('q.first_name', 'asc');
}
// get rows for this page
$obj_rows = $query->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery()
->getResult();
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// add row data
$row['id'] = $orow->getID();
$row['title'] = $orow->getTitle();
$row['first_name'] = $orow->getFirstName();
$row['last_name'] = $orow->getLastName();
$row['customer_classification'] = CustomerClassification::getName($orow->getCustomerClassification());
$row['flag_mobile_app'] = $orow->hasMobileApp();
$row['app_mobile_number'] = $orow->hasMobileApp() && !empty($orow->getMobileSessions()) ? $orow->getMobileSessions()[0]->getPhoneNumber() : '';
$row['flag_active'] = $orow->isActive();
$row['flag_csat'] = $orow->isCSAT();
// TODO: properly add mobile numbers and plate numbers as searchable/sortable fields, use doctrine events
// prepend the country code before each mobile number
$mobile_number_list = [];
$mobile_numbers = $orow->getMobileNumberList();
foreach ($mobile_numbers as $mobile_number)
{
$mobile_number_list[] = $this->country_code . $mobile_number;
}
$row['mobile_numbers'] = implode("<br>", $mobile_number_list);
$row['plate_numbers'] = implode("<br>", $orow->getPlateNumberList());
$rows[] = $row;
}
$params['meta'] = $meta;
$params['rows'] = $rows;
return $params;
}
// initialize add customer form
public function initializeAddCustomerForm()
{
$params['obj'] = new Customer();
$params['mode'] = 'create';
// get dropdown parameters
$this->fillDropdownParameters($params);
// get template to display
$params['template'] = $this->getTwigTemplate('cust_add_form');
// return params
return $params;
}
// add new customer and customer vehicle, if any
public function addCustomer(Request $req)
{
// create new row
$em = $this->em;
$row = new Customer();
$this->setObject($row, $req);
// initialize error lists
$error_array = [];
$nerror_array = [];
$verror_array = [];
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
if (!empty($vehicles))
{
foreach ($vehicles as $vehicle)
{
// check if vehicle exists
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if (empty($vobj))
{
$verror_array[$vehicle->index]['vehicle'] = 'Invalid vehicle specified.';
}
else
{
$cust_vehicle = new CustomerVehicle();
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor($vehicle->color)
->setStatusCondition($vehicle->status_condition)
->setFuelType($vehicle->fuel_type)
->setActive($vehicle->flag_active)
->setCustomer($row);
// if specified, check if battery exists
if ($vehicle->battery)
{
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if (empty($bobj))
{
$verror_array[$vehicle->index]['battery'] = 'Invalid battery specified.';
}
else
{
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
}
else
{
$cust_vehicle->setHasMotoliteBattery(false);
}
$verrors = $this->validator->validate($cust_vehicle);
// add errors to list
foreach ($verrors as $error)
{
if (!isset($verror_array[$vehicle->index]))
$verror_array[$vehicle->index] = [];
$verror_array[$vehicle->index][$error->getPropertyPath()] = $error->getMessage();
}
// add to entity
if (!isset($verror_array[$vehicle->index]))
{
$row->addVehicle($cust_vehicle);
}
}
}
}
// validate
$errors = $this->validator->validate($row);
// add errors to list
foreach ($errors as $error)
{
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
$result = [];
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array))
{
// return all error_arrays
$result = [
'error_array' => $error_array,
'nerror_array' => $nerror_array,
'verror_array' => $verror_array,
];
}
else
{
// validated! save the entity
$em->persist($row);
$em->flush();
$result = [
'id' => $row->getID(),
];
}
return $result;
}
// initialize update customer form
public function initializeUpdateCustomerForm($id)
{
$params['mode'] = 'update';
// get row data
$em = $this->em;
$row = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($row))
throw new NotFoundHttpException('The item does not exist');
// get dropdown parameters
$this->fillDropdownParameters($params);
// get template to display
$params['template'] = $this->getTwigTemplate('cust_update_form');
$params['obj'] = $row;
return $params;
}
// update customer and customer vehicle
public function updateCustomer(Request $req, $id)
{
// get row data
$em = $this->em;
$cust = $em->getRepository(Customer::class)->find($id);
// make sure this row exists
if (empty($cust))
throw $this->createNotFoundException('The item does not exist');
$this->setObject($cust, $req);
// initialize error lists
$error_array = [];
$nerror_array = [];
$verror_array = [];
// TODO: validate mobile numbers
// TODO: validate vehicles
// custom validation for vehicles
$vehicles = json_decode($req->request->get('vehicles'));
$this->updateVehicles($em, $cust, $vehicles);
// validate
$errors = $this->validator->validate($cust);
// add errors to list
foreach ($errors as $error)
{
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
$result = [];
if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array))
{
// return all error_arrays
$result = [
'error_array' => $error_array,
'nerror_array' => $nerror_array,
'verror_array' => $verror_array,
];
}
else
{
// validated! save the entity. do a persist anyway to save child entities
$em->persist($cust);
$em->flush();
$result = [
'id' => $cust->getID(),
];
}
return $result;
}
// delete customer
public function deleteCustomer(int $id)
{
// get row data
$em = $this->em;
$row = $em->getRepository(Customer::class)->find($id);
if (empty($row))
throw new NotFoundHttpException('The item does not exist');
// delete this row
$em->remove($row);
$em->flush();
}
// get customer vehicles
public function getCustomerVehicles(Request $req)
{
// get search term
$term = $req->query->get('search');
// get querybuilder
$qb = $this->em
->getRepository(CustomerVehicle::class)
->createQueryBuilder('q');
/*
// build expression now since we're reusing it
$vehicle_label = $qb->expr()->concat(
'q.plate_number',
$qb->expr()->literal(' - '),
'c.first_name',
$qb->expr()->literal(' '),
'c.last_name',
$qb->expr()->literal(' (+63'),
'c.phone_mobile',
$qb->expr()->literal(')')
);
*/
// count total records
$tquery = $qb->select('COUNT(q)');
// add filters to count query
if (!empty($term)) {
$tquery->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$tquery->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$tquery->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
$total = $tquery->getQuery()
->getSingleScalarResult();
// pagination vars
$page = $req->query->get('page') ?? 1;
$perpage = 20;
$offset = ($page - 1) * $perpage;
$pages = ceil($total / $perpage);
$has_more_pages = $page < $pages ? true : false;
// build main query
$query = $qb->select('q');
/*
->addSelect($vehicle_label . ' as vehicle_label')
->addSelect('c.first_name as cust_first_name')
->addSelect('c.last_name as cust_last_name');
*/
// add filters if needed
if (!empty($term)) {
$query->where('q.plate_number like :search')
->setParameter('search', $term . '%');
/*
$query->where('match_against (q.plate_number, :search \'in boolean mode\') > 0.1')
->setParameter('search', $term . '*');
*/
/*
$query->where('q.plate_number LIKE :filter')
->setParameter('filter', '%' . $term . '%');
*/
}
// get rows
$query_obj = $query->orderBy('q.plate_number', 'asc')
->setFirstResult($offset)
->setMaxResults($perpage)
->getQuery();
// error_log($query_obj->getSql());
$obj_rows = $query_obj->getResult();
// build vehicles array
$vehicles = [];
foreach ($obj_rows as $cv) {
$cust = $cv->getCustomer();
$vehicles[] = [
'id' => $cv->getID(),
'text' => $cv->getPlateNumber() . ' ' . $cust->getFirstName() . ' ' . $cust->getLastName() . ' ('. $this->country_code . $cust->getPhoneMobile() . ')',
];
}
$results = [
'vehicles' => $vehicles,
'has_more_pages' => $has_more_pages,
];
return $results;
}
// get customer vehicle info
public function getCustomerVehicleInfo(Request $req)
{
// get id
$id = $req->query->get('id');
// get row data
$em = $this->em;
$obj = $em->getRepository(CustomerVehicle::class)->find($id);
// make sure this row exists
if (empty($obj)) {
return null;
}
$customer = $obj->getCustomer();
$vehicle = $obj->getVehicle();
$battery = $obj->getCurrentBattery();
// build response
$row = [
'customer' => [
'id' => $customer->getID(),
'first_name' => $customer->getFirstName(),
'last_name' => $customer->getLastName(),
'customer_notes' => $customer->getCustomerNotes(),
'phone_mobile' => $customer->getPhoneMobile(),
'phone_landline' => $customer->getPhoneLandline(),
'phone_office' => $customer->getPhoneOffice(),
'phone_fax' => $customer->getPhoneFax(),
],
'vehicle' => [
'id' => $vehicle->getID(),
'mfg_name' => $vehicle->getManufacturer()->getName(),
'make' => $vehicle->getMake(),
'model_year_from' => $vehicle->getModelYearFrom(),
'model_year_to' => $vehicle->getModelYearTo(),
'model_year' => $obj->getModelYear(),
'color' => $obj->getColor(),
'plate_number' => $obj->getPlateNumber(),
'fuel_type' => $obj->getFuelType(),
'status_condition' => $obj->getStatusCondition(),
]
];
if (!empty($battery)) {
$row['battery'] = [
'id' => $battery->getID(),
'mfg_name' => $battery->getManufacturer()->getName(),
'model_name' => $battery->getModel()->getName(),
'size_name' => $battery->getSize()->getName(),
'prod_code' => $battery->getProductCode(),
'warranty_code' => $obj->getWarrantyCode(),
'warranty_expiration' => $obj->getWarrantyExpiration() ? $obj->getWarrantyExpiration()->format("d M Y") : "",
'has_motolite_battery' => $obj->hasMotoliteBattery(),
'is_active' => $obj->isActive()
];
}
return $row;
}
protected function getTwigTemplate($id)
{
if (isset($this->template_hash[$id]))
{
return $this->template_hash[$id];
}
return null;
}
protected function setObject($obj, $req)
{
// set and save values
$obj->setTitle($req->request->get('title'))
->setFirstName($req->request->get('first_name'))
->setLastName($req->request->get('last_name'))
->setCustomerClassification($req->request->get('customer_classification'))
->setCustomerNotes($req->request->get('customer_notes'))
->setEmail($req->request->get('email'))
->setIsCSAT($req->request->get('flag_csat') ? true : false)
->setActive($req->request->get('flag_active') ? true : false);
// phone numbers
$obj->setPhoneMobile($req->request->get('phone_mobile'))
->setPhoneLandline($req->request->get('phone_landline'))
->setPhoneOffice($req->request->get('phone_office'))
->setPhoneFax($req->request->get('phone_fax'));
}
protected function fillDropdownParameters(&$params)
{
$em = $this->em;
$params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['classifications'] = CustomerClassification::getCollection();
$params['fuel_types'] = FuelType::getCollection();
$params['status_conditions'] = VehicleStatusCondition::getCollection();
$params['years'] = $this->generateYearOptions();
$params['batteries'] = $em->getRepository(Battery::class)->findAll();
}
protected function generateYearOptions()
{
$start_year = 1950;
return range($start_year, date("Y") + 1);
}
protected function loadTemplates()
{
$this->template_hash = [];
// add all twig templates for customer to hash
$this->template_hash['cust_add_form'] = 'customer/form.html.twig';
$this->template_hash['cust_update_form'] = 'customer/form.html.twig';
$this->template_hash['cust_list'] = 'customer/list.html.twig';
}
protected function updateVehicles($em, Customer $cust, $vehicles)
{
$vehicle_ids = [];
foreach ($vehicles as $vehicle)
{
// check if customer vehicle exists
if (!empty($vehicle->id))
{
$cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($vehicle->id);
if ($cust_vehicle == null)
throw new CrudException("Could not find customer vehicle.");
}
// this is a new vehicle
else
{
$cust_vehicle = new CustomerVehicle();
$cust_vehicle->setCustomer($cust);
$cust->addVehicle($cust_vehicle);
$em->persist($cust_vehicle);
}
// vehicle, because they could have changed vehicle type
$vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle);
if ($vobj == null)
throw new CrudException("Could not find vehicle.");
// TODO: validate details
$cust_vehicle->setName($vehicle->name)
->setVehicle($vobj)
->setPlateNumber($vehicle->plate_number)
->setModelYear($vehicle->model_year)
->setColor($vehicle->color)
->setStatusCondition($vehicle->status_condition)
->setFuelType($vehicle->fuel_type)
->setActive($vehicle->flag_active);
// if specified, check if battery exists
if ($vehicle->battery)
{
// check if battery exists
$bobj = $em->getRepository(Battery::class)->find($vehicle->battery);
if ($bobj == null)
throw new CrudException("Could not find battery.");
// check if warranty expiration was specified
$warr_ex = DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration);
if (!$warr_ex)
$warr_ex = null;
$cust_vehicle->setHasMotoliteBattery(true)
->setCurrentBattery($bobj)
->setWarrantyCode($vehicle->warranty_code)
->setWarrantyExpiration($warr_ex);
}
else
{
$cust_vehicle->setHasMotoliteBattery(false);
}
// add to list of vehicles to keep
$vehicle_ids[$cust_vehicle->getID()] = true;
}
// cleanup
// delete all vehicles not in list
$cvs = $cust->getVehicles();
foreach ($cvs as $cv)
{
if (!isset($vehicle_ids[$cv->getID()]))
{
$cust->removeVehicle($cv);
$em->remove($cv);
}
}
}
// check if datatable filter is present and append to query
protected function setQueryFilters($datatable, &$query) {
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->join('q.vehicles', 'cv')
->where('q.first_name LIKE :filter')
->orWhere('q.last_name LIKE :filter')
->orWhere('q.customer_classification LIKE :filter')
->orWhere('cv.plate_number LIKE :filter')
->setParameter('filter', $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace App\Service;
use Symfony\Component\HttpFoundation\Request;
interface CustomerHandlerInterface
{
// initialize form to display customer list
public function initializeCustomerIndexForm();
// get customers
public function getCustomers(Request $req);
// initialize add customer form
public function initializeAddCustomerForm();
// add new customer and customer vehicle, if any
public function addCustomer(Request $req);
// initialize update customer form
public function initializeUpdateCustomerForm(int $id);
// update customer and customer vehicle
public function updateCustomer(Request $req, int $id);
// delete customer
public function deleteCustomer(int $id);
// get customer vehicles
public function getCustomerVehicles(Request $req);
// get customer vehicle info
public function getCustomerVehicleInfo(Request $req);
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Service\GISManager;
use App\Service\GISManagerInterface;
class Bing implements GISManagerInterface
{
const JS_INIT_FILE = 'initBingMap.js';
public function getJSInitFile()
{
// return the bing map js file: initBingMap.js
return self::JS_INIT_FILE;
}
public function getJSJOFile()
{
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Service\GISManager;
use App\Service\GISManagerInterface;
class Google implements GISManagerInterface
{
const JS_INIT_FILE = 'initGoogleMap.js';
public function getJSInitFile()
{
// return the google map js file: initGoogleMap.js
return self::JS_INIT_FILE;
}
public function getJSJOFile()
{
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Service\GISManager;
use App\Service\GISManagerInterface;
class OpenStreet implements GISManagerInterface
{
const JS_INIT_FILE = 'initOpenStreetMap.js';
const JS_JO_FILE = 'joOpenStreetMap.js';
public function getJSInitFile()
{
// return the openstreet map js file: initOpenStreetMap.js
return self::JS_INIT_FILE;
}
public function getJSJOFile()
{
return self::JS_JO_FILE;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Service;
interface GISManagerInterface
{
// returns the actual JS file
public function getJSInitFile();
// return the JS file for JO map
public function getJSJOFile();
}

View file

@ -0,0 +1,632 @@
<?php
namespace App\Service\InvoiceGenerator;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
use App\Ramcar\CMBTradeInType;
use App\Ramcar\DiscountApply;
use App\Ramcar\CMBServiceType;
use App\Ramcar\FuelType;
use App\Entity\Invoice;
use App\Entity\InvoiceItem;
use App\Entity\Battery;
use App\Entity\Promo;
use App\Entity\User;
use App\Service\InvoiceGeneratorInterface;
use Doctrine\Common\Util\Debug;
class CMBInvoiceGenerator implements InvoiceGeneratorInterface
{
const TAX_RATE = 0.00;
const SERVICE_FEE = 300;
const RECHARGE_FEE = 300;
const TROUBLESHOOTING_FEE = 150;
const BATT_REPLACEMENT_FEE = 0;
const WARRANTY_FEE = 0;
const OTHER_SERVICES_FEE = 200;
const COOLANT_FEE = 1600;
const REFUEL_FEE_GAS = 260;
const REFUEL_FEE_DIESEL = 220;
private $security;
protected $em;
protected $validator;
// creates invoice based on the criteria sent
public function __construct(Security $security, EntityManagerInterface $em,
ValidatorInterface $validator)
{
$this->security = $security;
$this->em = $em;
$this->validator = $validator;
}
public function generateInvoice(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();
switch ($stype)
{
case CMBServiceType::JUMPSTART:
$this->processJumpstart($total, $invoice);
break;
//case ServiceType::JUMPSTART_WARRANTY:
// $this->processJumpstartWarranty($total, $invoice);
case CMBServiceType::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 CMBServiceType::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();
// get current user
$user = $this->security->getUser();
if ($user != null)
{
$invoice->setCreatedBy($user);
}
$invoice->setTotalPrice($total['total_price'])
->setVATExclusivePrice($total['vat_ex_price'])
->setVAT($total['vat'])
->setDiscount($total['discount'])
->setTradeIn($total['ti_rate'])
->setStatus(InvoiceStatus::DRAFT);
// dump
//Debug::dump($invoice, 1);
return $invoice;
}
// generate invoice criteria
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array)
{
$em = $this->em;
// instantiate the invoice criteria
$criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
$ierror = $this->invoicePromo($criteria, $promo_id);
if (!$ierror && !empty($invoice_items))
{
// check for trade-in so we can mark it for mobile app
foreach ($invoice_items as $item)
{
// get first trade-in
if (!empty($item['trade_in']))
{
$jo->getTradeInType($item['trade_in']);
break;
}
}
$ierror = $this->invoiceBatteries($criteria, $invoice_items);
}
if ($ierror)
{
$error_array['invoice'] = $ierror;
}
else
{
// generate the invoice
$iobj = $this->generateInvoice($criteria);
// validate
$ierrors = $this->validator->validate($iobj);
// add errors to list
foreach ($ierrors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if invoice already exists for JO
$old_invoice = $jo->getInvoice();
if ($old_invoice != null)
{
// remove old invoice
$em->remove($old_invoice);
$em->flush();
}
// add invoice to JO
$jo->setInvoice($iobj);
$em->persist($iobj);
}
}
protected function getTaxAmount($price)
{
$vat_ex_price = $this->getTaxExclusivePrice($price);
return $price - $vat_ex_price;
// return round($vat_ex_price * self::TAX_RATE, 2);
}
protected function getTaxExclusivePrice($price)
{
return round($price / (1 + self::TAX_RATE), 2);
}
protected function getTradeInRate($ti)
{
$size = $ti['size'];
$trade_in = $ti['trade_in'];
if ($trade_in == null)
return 0;
switch ($trade_in)
{
// TODO: for now, tradein uses getTIPriceMotolite.
// Might need to modify later
case CMBTradeInType::YES:
return $size->getTIPriceMotolite();
}
return 0;
}
public function invoicePromo(InvoiceCriteria $criteria, $promo_id)
{
// return error if there's a problem, false otherwise
// check service type
$stype = $criteria->getServiceType();
if ($stype != CMBServiceType::BATTERY_REPLACEMENT_NEW)
return null;
if (empty($promo_id))
{
return false;
}
// check if this is a valid promo
$promo = $this->em->getRepository(Promo::class)->find($promo_id);
if (empty($promo))
return 'Invalid promo specified.';
$criteria->addPromo($promo);
return false;
}
public function invoiceBatteries(InvoiceCriteria $criteria, $items)
{
// check service type
$stype = $criteria->getServiceType();
if ($stype != CMBServiceType::BATTERY_REPLACEMENT_NEW && $stype != CMBServiceType::BATTERY_REPLACEMENT_WARRANTY)
return null;
// return error if there's a problem, false otherwise
if (!empty($items))
{
foreach ($items as $item)
{
// check if this is a valid battery
$battery = $this->em->getRepository(Battery::class)->find($item['battery']);
if (empty($battery))
{
$error = 'Invalid battery specified.';
return $error;
}
// quantity
$qty = $item['quantity'];
if ($qty < 1)
continue;
/*
// add to criteria
$criteria->addBattery($battery, $qty);
*/
// if this is a trade in, add trade in
if (!empty($item['trade_in']) && CMBTradeInType::validate($item['trade_in']))
$trade_in = $item['trade_in'];
else
$trade_in = null;
$criteria->addEntry($battery, $trade_in, $qty);
}
}
return null;
}
protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice)
{
// error_log('processing entries...');
$entries = $criteria->getEntries();
$con_batts = [];
$con_tis = [];
foreach ($entries as $entry)
{
$batt = $entry['battery'];
$qty = $entry['qty'];
$trade_in = $entry['trade_in'];
$size = $batt->getSize();
// consolidate batteries
$batt_id = $batt->getID();
if (!isset($con_batts[$batt_id]))
$con_batts[$batt->getID()] = [
'batt' => $batt,
'qty' => 0
];
$con_batts[$batt_id]['qty']++;
// no trade-in
if ($trade_in == null)
continue;
// consolidate trade-ins
$ti_key = $size->getID() . '|' . $trade_in;
if (!isset($con_tis[$ti_key]))
$con_tis[$ti_key] = [
'size' => $size,
'trade_in' => $trade_in,
'qty' => 0
];
$con_tis[$ti_key]['qty']++;
}
$this->processBatteries($total, $con_batts, $invoice);
$this->processTradeIns($total, $con_tis, $invoice);
}
protected function processBatteries(&$total, $con_batts, Invoice $invoice)
{
// process batteries
foreach ($con_batts as $con_data)
{
$batt = $con_data['batt'];
$qty = $con_data['qty'];
$sell_price = $batt->getSellingPrice();
$vat = $this->getTaxAmount($sell_price);
// $vat_ex_price = $this->getTaxExclusivePrice($sell_price);
$total['sell_price'] += $sell_price * $qty;
$total['vat'] += $vat * $qty;
$total['vat_ex_price'] += ($sell_price - $vat) * $qty;
$total['total_price'] += $sell_price * $qty;
// add item
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName())
->setQuantity($qty)
->setPrice($sell_price)
->setBattery($batt);
$invoice->addItem($item);
}
}
protected function processTradeIns(&$total, $con_tis, Invoice $invoice)
{
foreach ($con_tis as $ti)
{
$qty = $ti['qty'];
$ti_rate = $this->getTradeInRate($ti);
$total['ti_rate'] += $ti_rate * $qty;
$total['total_price'] -= $ti_rate * $qty;
// add item
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Trade-in ' . CMBTradeInType::getName($ti['trade_in']) . ' ' . $ti['size']->getName() . ' battery')
->setQuantity($qty)
->setPrice($ti_rate * -1);
$invoice->addItem($item);
}
}
protected function processDiscount(&$total, InvoiceCriteria $criteria, Invoice $invoice)
{
$promos = $criteria->getPromos();
if (count($promos) < 1)
return;
// NOTE: only get first promo because only one is applicable anyway
$promo = $promos[0];
$rate = $promo->getDiscountRate();
$apply_to = $promo->getDiscountApply();
switch ($apply_to)
{
case DiscountApply::SRP:
$discount = round($total['sell_price'] * $rate, 2);
break;
case DiscountApply::OPL:
// $discount = round($total['sell_price'] * 0.6 / 0.7 * $rate, 2);
$discount = round($total['sell_price'] * (1 - 1.5 / 0.7 * $rate), 2);
break;
}
// if discount is higher than 0, display in invoice
if ($discount > 0)
{
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Promo discount')
->setQuantity(1)
->setPrice(-1 * $discount);
$invoice->addItem($item);
}
$total['discount'] = $discount;
$total['total_price'] -= $discount;
// process
$invoice->setPromo($promo);
}
protected function processJumpstart(&$total, $invoice)
{
// add troubleshooting fee
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Troubleshooting fee')
->setQuantity(1)
->setPrice(self::TROUBLESHOOTING_FEE);
$invoice->addItem($item);
$total['sell_price'] = self::TROUBLESHOOTING_FEE;
$total['vat_ex_price'] = self::TROUBLESHOOTING_FEE;
$total['total_price'] = self::TROUBLESHOOTING_FEE;
}
protected function processJumpstartWarranty(&$total, $invoice)
{
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Troubleshooting fee')
->setQuantity(1)
->setPrice(0.00);
$invoice->addItem($item);
}
protected function processRecharge(&$total, $invoice)
{
// add recharge fee
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Recharge fee')
->setQuantity(1)
->setPrice(self::RECHARGE_FEE);
$invoice->addItem($item);
$total['sell_price'] = self::RECHARGE_FEE;
$total['vat_ex_price'] = self::RECHARGE_FEE;
$total['total_price'] = self::RECHARGE_FEE;
}
protected function processReplacement(&$total, $invoice)
{
// add recharge fee
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Battery replacement')
->setQuantity(1)
->setPrice(self::BATT_REPLACEMENT_FEE);
$invoice->addItem($item);
}
protected function processWarranty(&$total, InvoiceCriteria $criteria, $invoice)
{
// error_log('processing warranty');
$entries = $criteria->getEntries();
foreach ($entries as $entry)
{
$batt = $entry['battery'];
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName() . ' - Service Unit')
->setQuantity(1)
->setPrice(self::WARRANTY_FEE)
->setBattery($batt);
$invoice->addItem($item);
}
}
protected function processOtherServices(&$total, $invoice, $stype)
{
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Service - ' . CMBServiceType::getName($stype))
->setQuantity(1)
->setPrice(self::OTHER_SERVICES_FEE);
$invoice->addItem($item);
$total['total_price'] = 200.00;
}
protected function processOverheat(&$total, $invoice, $cv, $has_coolant)
{
// free if they have a motolite battery
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = self::SERVICE_FEE;
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Service - Overheat Assistance')
->setQuantity(1)
->setPrice($fee);
$invoice->addItem($item);
$total_price = $fee;
if ($has_coolant)
{
$coolant = new InvoiceItem();
$coolant->setInvoice($invoice)
->setTitle('4L Coolant')
->setQuantity(1)
->setPrice(self::COOLANT_FEE);
$invoice->addItem($coolant);
$total_price += self::COOLANT_FEE;
//$total_price += 1600;
}
$vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat;
}
protected function processTireRepair(&$total, $invoice, $cv)
{
// free if they have a motolite battery
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = self::SERVICE_FEE;
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Service - Flat Tire')
->setQuantity(1)
->setPrice($fee);
$invoice->addItem($item);
$total_price = $fee;
$vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat;
}
protected function processRefuel(&$total, $invoice, $cv)
{
// free if they have a motolite battery
if ($cv->hasMotoliteBattery())
$fee = 0;
else
$fee = self::SERVICE_FEE;
$ftype = $cv->getFuelType();
$item = new InvoiceItem();
// service charge
$item->setInvoice($invoice)
->setTitle('Service - ' . CMBServiceType::getName(CMBServiceType::EMERGENCY_REFUEL))
->setQuantity(1)
->setPrice($fee);
$invoice->addItem($item);
$total_price = $fee;
// $total['total_price'] = 200.00;
$gas_price = self::REFUEL_FEE_GAS;
$diesel_price = self::REFUEL_FEE_DIESEL;
$fuel = new InvoiceItem();
error_log('fuel type - ' . $ftype);
switch ($ftype)
{
case FuelType::GAS:
$fuel->setInvoice($invoice)
->setTitle('4L Fuel - Gas')
->setQuantity(1)
->setPrice($gas_price);
$invoice->addItem($fuel);
$total_price += $gas_price;
break;
case FuelType::DIESEL:
$fuel->setInvoice($invoice)
->setTitle('4L Fuel - Diesel')
->setQuantity(1)
->setPrice($diesel_price);
$total_price += $diesel_price;
$invoice->addItem($fuel);
break;
default:
// NOTE: should never get to this point
$fuel->setInvoice($invoice)
->setTitle('Fuel - Unknown')
->setQuantity(1)
->setPrice(0);
$total_price += 0.00;
$invoice->addItem($fuel);
break;
}
$vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat;
}
}

View file

@ -1,8 +1,14 @@
<?php <?php
namespace App\Service; namespace App\Service\InvoiceGenerator;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\ORM\EntityManagerInterface;
use App\Ramcar\InvoiceCriteria; use App\Ramcar\InvoiceCriteria;
use App\Ramcar\InvoiceStatus;
use App\Ramcar\TradeInType; use App\Ramcar\TradeInType;
use App\Ramcar\DiscountApply; use App\Ramcar\DiscountApply;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
@ -11,32 +17,195 @@ use App\Ramcar\FuelType;
use App\Entity\Invoice; use App\Entity\Invoice;
use App\Entity\InvoiceItem; use App\Entity\InvoiceItem;
use App\Entity\User; use App\Entity\User;
use App\Entity\Battery;
use App\Service\InvoiceGeneratorInterface;
use Doctrine\Common\Util\Debug; use Doctrine\Common\Util\Debug;
class InvoiceCreator class ResqInvoiceGenerator implements InvoiceGeneratorInterface
{ {
const VAT_RATE = 0.12; const TAX_RATE = 0.12;
const SERVICE_FEE = 300; const SERVICE_FEE = 300;
const RECHARGE_FEE = 300;
const TROUBLESHOOTING_FEE = 150;
const BATT_REPLACEMENT_FEE = 0;
const WARRANTY_FEE = 0;
const OTHER_SERVICES_FEE = 200;
const COOLANT_FEE = 1600;
const REFUEL_FEE_GAS = 260;
const REFUEL_FEE_DIESEL = 220;
private $security;
protected $em;
protected $validator;
// creates invoice based on the criteria sent // creates invoice based on the criteria sent
public function __construct() public function __construct(Security $security, EntityManagerInterface $em,
ValidatorInterface $validator)
{ {
$this->security = $security;
$this->em = $em;
$this->validator = $validator;
} }
public function getVATAmount($price) public function generateInvoice(InvoiceCriteria $criteria)
{ {
$vat_ex_price = $this->getVATExclusivePrice($price); // 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();
// get current user
$user = $this->security->getUser();
if ($user != null)
{
$invoice->setCreatedBy($user);
}
$invoice->setTotalPrice($total['total_price'])
->setVATExclusivePrice($total['vat_ex_price'])
->setVAT($total['vat'])
->setDiscount($total['discount'])
->setTradeIn($total['ti_rate'])
->setStatus(InvoiceStatus::DRAFT);
// dump
//Debug::dump($invoice, 1);
return $invoice;
}
// generate invoice criteria
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array)
{
$em = $this->em;
// instantiate the invoice criteria
$criteria = new InvoiceCriteria();
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
$ierror = $this->invoicePromo($criteria, $promo_id);
if (!$ierror && !empty($invoice_items))
{
// check for trade-in so we can mark it for mobile app
foreach ($invoice_items as $item)
{
// get first trade-in
if (!empty($item['trade_in']))
{
$jo->getTradeInType($item['trade_in']);
break;
}
}
$ierror = $this->invoiceBatteries($criteria, $invoice_items);
}
if ($ierror)
{
$error_array['invoice'] = $ierror;
}
else
{
// generate the invoice
$iobj = $this->generateInvoice($criteria);
// validate
$ierrors = $this->validator->validate($iobj);
// add errors to list
foreach ($ierrors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if invoice already exists for JO
$old_invoice = $jo->getInvoice();
if ($old_invoice != null)
{
// remove old invoice
$em->remove($old_invoice);
$em->flush();
}
// add invoice to JO
$jo->setInvoice($iobj);
$em->persist($iobj);
}
}
protected function getTaxAmount($price)
{
$vat_ex_price = $this->getTaxExclusivePrice($price);
return $price - $vat_ex_price; return $price - $vat_ex_price;
// return round($vat_ex_price * self::VAT_RATE, 2); // return round($vat_ex_price * self::TAX_RATE, 2);
} }
public function getVATExclusivePrice($price) protected function getTaxExclusivePrice($price)
{ {
return round($price / (1 + self::VAT_RATE), 2); return round($price / (1 + self::TAX_RATE), 2);
} }
public function getTradeInRate($ti) protected function getTradeInRate($ti)
{ {
$size = $ti['size']; $size = $ti['size'];
$trade_in = $ti['trade_in']; $trade_in = $ti['trade_in'];
@ -57,6 +226,75 @@ class InvoiceCreator
return 0; return 0;
} }
public function invoicePromo(InvoiceCriteria $criteria, $promo_id)
{
// return error if there's a problem, false otherwise
// check service type
$stype = $criteria->getServiceType();
if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW)
return null;
if (empty($promo_id))
{
return false;
}
// check if this is a valid promo
$promo = $this->em->getRepository(Promo::class)->find($promo_id);
if (empty($promo))
return 'Invalid promo specified.';
$criteria->addPromo($promo);
return false;
}
public function invoiceBatteries(InvoiceCriteria $criteria, $items)
{
// check service type
$stype = $criteria->getServiceType();
if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW && $stype != ServiceType::BATTERY_REPLACEMENT_WARRANTY)
return null;
// return error if there's a problem, false otherwise
if (!empty($items))
{
foreach ($items as $item)
{
// check if this is a valid battery
$battery = $this->em->getRepository(Battery::class)->find($item['battery']);
if (empty($battery))
{
$error = 'Invalid battery specified.';
return $error;
}
// quantity
$qty = $item['quantity'];
if ($qty < 1)
continue;
/*
// add to criteria
$criteria->addBattery($battery, $qty);
*/
// if this is a trade in, add trade in
if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in']))
$trade_in = $item['trade_in'];
else
$trade_in = null;
$criteria->addEntry($battery, $trade_in, $qty);
}
}
return null;
}
protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice) protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice)
{ {
// error_log('processing entries...'); // error_log('processing entries...');
@ -109,8 +347,8 @@ class InvoiceCreator
$qty = $con_data['qty']; $qty = $con_data['qty'];
$sell_price = $batt->getSellingPrice(); $sell_price = $batt->getSellingPrice();
$vat = $this->getVATAmount($sell_price); $vat = $this->getTaxAmount($sell_price);
// $vat_ex_price = $this->getVATExclusivePrice($sell_price); // $vat_ex_price = $this->getTaxExclusivePrice($sell_price);
$total['sell_price'] += $sell_price * $qty; $total['sell_price'] += $sell_price * $qty;
$total['vat'] += $vat * $qty; $total['vat'] += $vat * $qty;
@ -192,22 +430,22 @@ class InvoiceCreator
$invoice->setPromo($promo); $invoice->setPromo($promo);
} }
public function processJumpstart(&$total, $invoice) protected function processJumpstart(&$total, $invoice)
{ {
// add troubleshooting fee // add troubleshooting fee
$item = new InvoiceItem(); $item = new InvoiceItem();
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle('Troubleshooting fee') ->setTitle('Troubleshooting fee')
->setQuantity(1) ->setQuantity(1)
->setPrice(150.00); ->setPrice(self::TROUBLESHOOTING_FEE);
$invoice->addItem($item); $invoice->addItem($item);
$total['sell_price'] = 150.00; $total['sell_price'] = self::TROUBLESHOOTING_FEE;
$total['vat_ex_price'] = 150.00; $total['vat_ex_price'] = self::TROUBLESHOOTING_FEE;
$total['total_price'] = 150.00; $total['total_price'] = self::TROUBLESHOOTING_FEE;
} }
public function processJumpstartWarranty(&$total, $invoice) protected function processJumpstartWarranty(&$total, $invoice)
{ {
$item = new InvoiceItem(); $item = new InvoiceItem();
$item->setInvoice($invoice) $item->setInvoice($invoice)
@ -217,33 +455,33 @@ class InvoiceCreator
$invoice->addItem($item); $invoice->addItem($item);
} }
public function processRecharge(&$total, $invoice) protected function processRecharge(&$total, $invoice)
{ {
// add recharge fee // add recharge fee
$item = new InvoiceItem(); $item = new InvoiceItem();
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle('Recharge fee') ->setTitle('Recharge fee')
->setQuantity(1) ->setQuantity(1)
->setPrice(300.00); ->setPrice(self::RECHARGE_FEE);
$invoice->addItem($item); $invoice->addItem($item);
$total['sell_price'] = 300.00; $total['sell_price'] = self::RECHARGE_FEE;
$total['vat_ex_price'] = 300.00; $total['vat_ex_price'] = self::RECHARGE_FEE;
$total['total_price'] = 300.00; $total['total_price'] = self::RECHARGE_FEE;
} }
public function processReplacement(&$total, $invoice) protected function processReplacement(&$total, $invoice)
{ {
// add recharge fee // add recharge fee
$item = new InvoiceItem(); $item = new InvoiceItem();
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle('Battery replacement') ->setTitle('Battery replacement')
->setQuantity(1) ->setQuantity(1)
->setPrice(0.00); ->setPrice(self::BATT_REPLACEMENT_FEE);
$invoice->addItem($item); $invoice->addItem($item);
} }
public function processWarranty(&$total, InvoiceCriteria $criteria, $invoice) protected function processWarranty(&$total, InvoiceCriteria $criteria, $invoice)
{ {
// error_log('processing warranty'); // error_log('processing warranty');
$entries = $criteria->getEntries(); $entries = $criteria->getEntries();
@ -254,25 +492,25 @@ class InvoiceCreator
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName() . ' - Service Unit') ->setTitle($batt->getModel()->getName() . ' ' . $batt->getSize()->getName() . ' - Service Unit')
->setQuantity(1) ->setQuantity(1)
->setPrice(0.00) ->setPrice(self::WARRANTY_FEE)
->setBattery($batt); ->setBattery($batt);
$invoice->addItem($item); $invoice->addItem($item);
} }
} }
public function processOtherServices(&$total, $invoice, $stype) protected function processOtherServices(&$total, $invoice, $stype)
{ {
$item = new InvoiceItem(); $item = new InvoiceItem();
$item->setInvoice($invoice) $item->setInvoice($invoice)
->setTitle('Service - ' . ServiceType::getName($stype)) ->setTitle('Service - ' . ServiceType::getName($stype))
->setQuantity(1) ->setQuantity(1)
->setPrice(200.00); ->setPrice(self::OTHER_SERVICES_FEE);
$invoice->addItem($item); $invoice->addItem($item);
$total['total_price'] = 200.00; $total['total_price'] = 200.00;
} }
public function processOverheat(&$total, $invoice, $cv, $has_coolant) protected function processOverheat(&$total, $invoice, $cv, $has_coolant)
{ {
// free if they have a motolite battery // free if they have a motolite battery
if ($cv->hasMotoliteBattery()) if ($cv->hasMotoliteBattery())
@ -295,20 +533,21 @@ class InvoiceCreator
$coolant->setInvoice($invoice) $coolant->setInvoice($invoice)
->setTitle('4L Coolant') ->setTitle('4L Coolant')
->setQuantity(1) ->setQuantity(1)
->setPrice(1600); ->setPrice(self::COOLANT_FEE);
$invoice->addItem($coolant); $invoice->addItem($coolant);
$total_price += 1600; $total_price += self::COOLANT_FEE;
//$total_price += 1600;
} }
$vat_ex_price = $this->getVATExclusivePrice($total_price); $vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price; $vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price; $total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price; $total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat; $total['vat'] = $vat;
} }
public function processTireRepair(&$total, $invoice, $cv) protected function processTireRepair(&$total, $invoice, $cv)
{ {
// free if they have a motolite battery // free if they have a motolite battery
if ($cv->hasMotoliteBattery()) if ($cv->hasMotoliteBattery())
@ -324,14 +563,14 @@ class InvoiceCreator
$invoice->addItem($item); $invoice->addItem($item);
$total_price = $fee; $total_price = $fee;
$vat_ex_price = $this->getVATExclusivePrice($total_price); $vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price; $vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price; $total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price; $total['vat_ex_price'] = $vat_ex_price;
$total['vat'] = $vat; $total['vat'] = $vat;
} }
public function processRefuel(&$total, $invoice, $cv) protected function processRefuel(&$total, $invoice, $cv)
{ {
// free if they have a motolite battery // free if they have a motolite battery
if ($cv->hasMotoliteBattery()) if ($cv->hasMotoliteBattery())
@ -351,8 +590,8 @@ class InvoiceCreator
$total_price = $fee; $total_price = $fee;
// $total['total_price'] = 200.00; // $total['total_price'] = 200.00;
$gas_price = 260; $gas_price = self::REFUEL_FEE_GAS;
$diesel_price = 220; $diesel_price = self::REFUEL_FEE_DIESEL;
$fuel = new InvoiceItem(); $fuel = new InvoiceItem();
//error_log('fuel type - ' . $ftype); //error_log('fuel type - ' . $ftype);
@ -385,7 +624,7 @@ class InvoiceCreator
break; break;
} }
$vat_ex_price = $this->getVATExclusivePrice($total_price); $vat_ex_price = $this->getTaxExclusivePrice($total_price);
$vat = $total_price - $vat_ex_price; $vat = $total_price - $vat_ex_price;
$total['total_price'] = $total_price; $total['total_price'] = $total_price;
$total['vat_ex_price'] = $vat_ex_price; $total['vat_ex_price'] = $vat_ex_price;

View file

@ -0,0 +1,18 @@
<?php
namespace App\Service;
use App\Entity\Invoice;
use App\Entity\JobOrder;
use App\Ramcar\InvoiceCriteria;
interface InvoiceGeneratorInterface
{
// generate invoice using a criteria
public function generateInvoice(InvoiceCriteria $criteria);
// generate invoice criteria
public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, array &$error_array);
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Service;
use App\Service\RedisClientProvider;
use App\Entity\JobOrder;
class JobOrderCache
{
protected $redis;
protected $active_jo_key;
public function __construct(RedisClientProvider $redis_prov, $active_jo_key)
{
$this->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()
);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
<?php
namespace App\Service;
use Symfony\Component\HttpFoundation\Request;
use App\Service\MQTTClient;
use App\Service\APNSClient;
use App\Service\MapTools;
interface JobOrderHandlerInterface
{
// TODO: event sending has been moved to rider assignment handler for cmb. Might need
// to consider resq implementation for event sending for the other methods.
// get job order rows
public function getRows(Request $req, string $tier);
// get job orders
public function getJobOrders(Request $req);
// generate job order
public function generateJobOrder(Request $req, int $id);
// generate one step job order
public function generateOneStepJobOrder(Request $req, int $id);
// dispatch job order
public function dispatchJobOrder(Request $req, int $id, MQTTClient $mclient);
// assign job order
public function assignJobOrder(Request $req, int $id, MQTTCLient $mclient, APNSClient $aclient);
// fulfill job order
public function fulfillJobOrder(Request $req, int $id, MQTTClient $mclient);
// cancel job order
public function cancelJobOrder(Request $req, int $id, MQTTClient $mclient);
// set hub for job order
public function setHub(Request $req, int $id, MQTTClient $mclient);
// reject hub for job order
public function rejectHub(Request $req, int $id);
// set rider for job order
public function setRider(Request $req, int $id, MQTTClient $mclient);
// unlock processor
public function unlockProcessor(int $id);
// unlock assignor
public function unlockAssignor(int $id);
// initialize incoming job order form
public function initializeIncomingForm();
// initialize open edit job order form
public function initializeOpenEditForm(int $id);
// initialize incoming vehicle form
public function initializeIncomingVehicleForm(int $cvid);
// initialize all job orders form for a specific job order id
public function initializeAllForm(int $id);
// initialize dispatch/processing job order form
public function initializeProcessingForm(int $id, MapTools $map_tools);
// initialize assign job order form
public function initializeAssignForm(int $id);
// initialize fulflll job order form
public function initializeFulfillmentForm(int $id);
// initialize hub form
public function initializeHubForm(int $id, MapTools $map_tools);
// initialize rider form
public function initializeRiderForm(int $id);
// initialize one step form
public function initializeOneStepForm();
// initialize one step edit form
public function initializeOneStepEditForm(int $id, MapTools $map_tools);
// generate pdf form for job order
public function generatePDFForm(Request $req, int $id, string $proj_path);
// get template to display
public function getTwigTemplate(string $id);
}

View file

@ -9,14 +9,15 @@ class MQTTClient
{ {
const PREFIX = 'motolite.control.'; const PREFIX = 'motolite.control.';
const RIDER_PREFIX = 'motorider_'; const RIDER_PREFIX = 'motorider_';
const REDIS_KEY = 'events';
// protected $mclient; // protected $mclient;
protected $redis; protected $redis;
protected $key;
public function __construct(RedisClientProvider $redis_client) public function __construct(RedisClientProvider $redis_client, $key)
{ {
$this->redis = $redis_client->getRedisClient(); $this->redis = $redis_client->getRedisClient();
$this->key = $key;
} }
public function __destruct() public function __destruct()
@ -29,7 +30,7 @@ class MQTTClient
// $this->mclient->publish($channel, $message); // $this->mclient->publish($channel, $message);
$data = $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) public function sendEvent(JobOrder $job_order, $payload)

View file

@ -84,10 +84,23 @@ class MapTools
{ {
//error_log($row[0]->getName() . ' - ' . $row['dist']); //error_log($row[0]->getName() . ' - ' . $row['dist']);
$hubs[] = $row[0]; $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[] = [ $final_data[] = [
'hub' => $row[0], 'hub' => $row[0],
'db_distance' => $row['dist'], 'db_distance' => $row['dist'],
'distance' => 0, 'distance' => $dist,
'duration' => 0, 'duration' => 0,
]; ];
} }
@ -135,4 +148,18 @@ class MapTools
return $final_data; 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);
}
} }

View file

@ -11,34 +11,46 @@ class RedisClientProvider
protected $host; protected $host;
protected $port; protected $port;
protected $password; 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->scheme = $scheme;
$this->host = $host; $this->host = $host;
$this->port = $port; $this->port = $port;
$this->password = $password; $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() 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; return $this->redis;
} }
} }

View file

@ -0,0 +1,98 @@
<?php
namespace App\Service\RiderAssignmentHandler;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\RiderAssignmentHandlerInterface;
use App\Service\MQTTClient;
use App\Service\APNSClient;
use App\Entity\JobOrder;
use App\Entity\Rider;
use App\Ramcar\JOStatus;
class CMBRiderAssignmentHandler implements RiderAssignmentHandlerInterface
{
protected $em;
protected $aclient;
protected $mclient;
public function __construct(EntityManagerInterface $em, MQTTClient $mclient,
APNSClient $aclient)
{
$this->em = $em;
$this->mclient = $mclient;
$this->aclient = $aclient;
}
// assign job order to rider
public function assignJobOrder(JobOrder $obj, Rider $rider)
{
// create the payload
$payload = [
'event' => 'driver_assigned'
];
// send event
$this->mclient->sendEvent($obj, $payload);
// check if rider is available
if ($rider->isAvailable())
{
error_log('set rider availability to false');
// set rider to unavailable
$rider->setAvailable(false);
// send event to rider
$this->mclient->sendRiderEvent($obj, $payload);
// send push notification
$this->aclient->sendPush($obj, "A RESQ rider is on his way to you.");
}
}
// complete job order
public function fulfillJobOrder(JobOrder $obj, string $image_url, Rider $rider)
{
// send to mqtt
$payload = [
'event' => 'fulfilled',
'jo_id' => $obj->getID(),
'driver_image' => $image_url,
'driver_name' => $rider->getFullName(),
'driver_id' => $rider->getID(),
];
$this->mclient->sendEvent($obj, $payload);
// send fulfill/complete event to rider
$this->mclient->sendRiderEvent($obj, $payload);
// search for the JO assigned to rider with JOStatus::ASSIGNED and sort by assign date
$jo_results = $this->em->getRepository(JobOrder::class)->findBy(['status' => JOStatus::ASSIGNED, 'rider' => $rider->getID()],
['date_assign' => 'ASC']);
// check if jo_results is empty
if (!empty($jo_results))
{
error_log('rider has another JO in queue');
// get first entry
$jo = current($jo_results);
// form the payload for the next job order
$jo_payload = [
'event' => 'driver_assigned'
];
// set rider to unavailable
$rider->setAvailable(false);
// send event to rider
$this->mclient->sendRiderEvent($jo, $jo_payload);
// send push notification
$this->aclient->sendPush($obj, "A RESQ rider is on his way to you.");
}
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace App\Service;
use App\Entity\JobOrder;
use App\Entity\Rider;
interface RiderAssignmentHandlerInterface
{
// assign job order to rider
public function assignJobOrder(JobOrder $obj, Rider $rider);
// complete job order
public function fulfillJobOrder(JobOrder $obj, string $image_url, Rider $rider);
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Service;
use App\Service\RedisClientProvider;
use App\Entity\Rider;
class RiderCache
{
protected $redis;
protected $loc_key;
protected $status_key;
public function __construct(RedisClientProvider $redis_prov, $loc_key, $status_key)
{
$this->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);
}
}

View file

@ -38,15 +38,18 @@ class RiderTracker
$long = $this->redis->hget($key, 'longitude'); $long = $this->redis->hget($key, 'longitude');
$lat = $this->redis->hget($key, 'latitude'); $lat = $this->redis->hget($key, 'latitude');
$coordinates = new Point($long, $lat); return new Point($long, $lat);
} }
else
{ // not in cache, get hub
$rider = $this->em->getRepository(Rider::class)->find($rider_id); $rider = $this->em->getRepository(Rider::class)->find($rider_id);
$coordinates = $rider->getHub()->getCoordinates(); $hub = $rider->getHub();
}
return $coordinates; // no hub
// TODO: return valid coordinate
if ($hub == null)
return new Point(0, 0);
return $hub->getCoordinates();
} }
} }

View file

@ -152,6 +152,9 @@
"setasign/fpdf": { "setasign/fpdf": {
"version": "1.8.1" "version": "1.8.1"
}, },
"symfony/asset": {
"version": "v4.4.3"
},
"symfony/cache": { "symfony/cache": {
"version": "v4.0.2" "version": "v4.0.2"
}, },

View file

@ -35,12 +35,16 @@
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<!--begin::Extra Styles --> <!--begin::Extra Styles -->
{% block stylesheets %}{% endblock %} {% block stylesheets %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/>
{% endblock %}
<!--end::Extra Styles --> <!--end::Extra Styles -->
</head> </head>
<!-- end::Head --> <!-- end::Head -->
<!-- end::Body --> <!-- end::Body -->
<body class="m-page--fluid m--skin- m-content--skin-light2 m-header--fixed m-header--fixed-mobile m-aside-left--enabled m-aside-left--skin-dark m-aside-left--offcanvas m-footer--push m-aside--offcanvas-default"> <body class="m-page--fluid m--skin- m-content--skin-light2 m-header--fixed m-header--fixed-mobile m-aside-left--enabled m-aside-left--skin-dark m-aside-left--offcanvas m-aside--offcanvas-default">
<!-- begin:: Page --> <!-- begin:: Page -->
<div class="m-grid m-grid--hor m-grid--root m-page"> <div class="m-grid m-grid--hor m-grid--root m-page">
<!-- BEGIN: Header --> <!-- BEGIN: Header -->
@ -695,30 +699,6 @@
</div> </div>
<!-- end:: Body --> <!-- end:: Body -->
<!-- begin::Footer --> <!-- begin::Footer -->
<footer class="m-grid__item m-footer">
<div class="m-container m-container--fluid m-container--full-height m-page__container">
<div class="m-stack m-stack--flex-tablet-and-mobile m-stack--ver m-stack--desktop">
<div class="m-stack__item m-stack__item--left m-stack__item--middle m-stack__item--last">
<span class="m-footer__copyright">
{{ "now"|date("Y") }} &copy; {% trans %}copyright{% endtrans %}
</span>
</div>
<div class="m-stack__item m-stack__item--right m-stack__item--middle m-stack__item--first">
<ul class="m-footer__nav m-nav m-nav--inline m--pull-right">
<!--
<li class="m-nav__item">
<a href="#" class="m-nav__link">
<span class="m-nav__link-text">
About
</span>
</a>
</li>
-->
</ul>
</div>
</div>
</div>
</footer>
<!-- end::Footer --> <!-- end::Footer -->
</div> </div>
<!-- end:: Page --> <!-- end:: Page -->

View file

@ -0,0 +1,70 @@
{% import 'menu.html.twig' as menu %}
<!DOCTYPE html>
<html lang="en">
<!-- begin::Head -->
<head>
<meta charset="UTF-8">
<title>{% block title %}{% trans %}block_title{% endtrans %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5">
<!--begin::Web font -->
<script src="https://ajax.googleapis.com/ajax/libs/webfont/1.6.16/webfont.js"></script>
<script>
WebFont.load({
google: {"families":["Poppins:300,400,500,600,700","Roboto:300,400,500,600,700"]},
active: function() {
sessionStorage.fonts = true;
}
});
</script>
<!--end::Web font -->
<!--begin::Base Styles -->
<!--begin::Page Vendors -->
<link href="/assets/vendors/custom/fullcalendar/fullcalendar.bundle.css" rel="stylesheet" type="text/css" />
<!--end::Page Vendors -->
<link href="/assets/vendors/base/vendors.bundle.css" rel="stylesheet" type="text/css" />
<link href="/assets/demo/default/base/style.bundle.css" rel="stylesheet" type="text/css" />
<link href="/assets/css/style.css" rel="stylesheet" type="text/css" />
<!--end::Base Styles -->
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{% trans %}icon_base_32x32{% endtrans %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% trans %}icon_base_16x16{% endtrans %}">
<link rel="manifest" href="/assets/images/favicon/manifest.json">
<meta name="theme-color" content="#ffffff">
<!--begin::Extra Styles -->
{% block stylesheets %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
crossorigin=""/>
{% endblock %}
<!--end::Extra Styles -->
</head>
<!-- end::Head -->
<!-- end::Body -->
<body class="m-page--fluid m--skin- m-content--skin-light2 m-aside-left--enabled m-aside-left--skin-dark m-aside-left--offcanvas m-aside--offcanvas-default">
<!-- begin:: Page -->
{% block body %}{% endblock %}
<!-- end:: Page -->
<!--begin::Base Scripts -->
<script src="/assets/vendors/base/vendors.bundle.js" type="text/javascript"></script>
<script src="/assets/demo/default/base/scripts.bundle.js" type="text/javascript"></script>
<!--end::Base Scripts -->
<!--begin::Page Vendors -->
<script src="/assets/vendors/custom/fullcalendar/fullcalendar.bundle.js" type="text/javascript"></script>
<!--end::Page Vendors -->
<!--begin::Page Snippets -->
<script src="/assets/app/js/dashboard.js" type="text/javascript"></script>
<script src="/assets/js/common.js" type="text/javascript"></script>
<!--end::Page Snippets -->
<!--begin::Extra Scripts -->
{% block scripts %}{% endblock %}
<!--end::Extra Scripts -->
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -600,7 +600,7 @@
// input mask // input mask
$("#mobile-number").inputmask("mask", { $("#mobile-number").inputmask("mask", {
mask: "{% trans %}country_code_prefix{% endtrans %}9999999999", mask: "639999999999",
placeholder: "" placeholder: ""
}); });
@ -1187,3 +1187,4 @@
}); });
</script> </script>
{% endblock %} {% endblock %}

View file

@ -96,7 +96,10 @@ initMap();
function initMap() { function initMap() {
var map = new google.maps.Map(document.getElementById('m_gmap'), var map = new google.maps.Map(document.getElementById('m_gmap'),
{ {
center: {lat: 14.6091, lng: 121.0223}, center: {
lat: {% trans %}default_lat{% endtrans %},
lng: {% trans %}default_long{% endtrans %},
},
mapTypeId: 'roadmap', mapTypeId: 'roadmap',
zoom: 13 zoom: 13
}); });

View file

@ -1,32 +1,90 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block body %} {% block stylesheets %}
<!-- BEGIN: Subheader --> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/>
<div class="m-subheader"> {% endblock %}
<div class="d-flex align-items-center">
<div class="mr-auto"> {% block body %}
<h3 class="m-subheader__title"> <div id="dashboard_map" style="height:100%;"></div>
Dashboard <!-- BEGIN: Subheader -->
</h3> <!-- END: Subheader -->
</div> {% endblock %}
<div>
<span class="m-subheader__daterange" id="m_dashboard_daterangepicker">
<span class="m-subheader__daterange-label"> {% block scripts %}
<span class="m-subheader__daterange-title"></span> <script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<span class="m-subheader__daterange-date m--font-brand"></span> <script src="{{ asset('assets/js/dashboard_map.js') }}"></script>
</span> <script src="{{ asset('assets/js/map_mqtt.js') }}"></script>
<a href="#" class="btn btn-sm btn-brand m-btn m-btn--icon m-btn--icon-only m-btn--custom m-btn--pill"> {{ include('map/' ~ map_js_file) }}
<i class="la la-angle-down"></i> <script>
</a>
</span> function initMap(r_markers, c_markers, icons) {
</div> var default_lat = {% trans %}default_lat{% endtrans %};
</div> var default_lng = {% trans %}default_long{% endtrans %};
</div>
<!-- END: Subheader --> var options = {
<div class="m-content"> 'display_overlay': true,
<!--Begin::Section--> 'enable_popup': true,
<!--End::Section--> 'access_token': 'pk.eyJ1Ijoia2NvcmRlcm8iLCJhIjoiY2szbzA3ZHdsMDZxdTNsbGl4ZGNnN2VxaSJ9.LRzAe3RlV8sIP1N1x0chdw',
<!--Begin::Section--> 'div_id': 'dashboard_map',
<!--End::Section--> 'center_lat': default_lat,
</div> 'center_lng': default_lng,
'map_type': 'road',
'zoom': 13,
'rider_popup_url': '/riders/[id]/popup',
'cust_popup_url': '/job-order/[id]/popup',
'icons': icons
};
var dashmap = new DashboardMap(options, r_markers, c_markers);
dashmap.initialize();
dashmap.loadLocations('{{ path('rider_locations') }}');
return dashmap;
}
function initEventHandler(dashmap) {
var options = {
'track_jo': true,
'track_rider': true,
'channels': {
'rider_location': 'rider/+/location',
'jo_location': 'jo/+/location',
'jo_status': 'jo/+/status'
},
};
var event_handler = new MapEventHandler(options, dashmap);
event_handler.connect('{{ app.user.getID }}', '{{ mqtt_host }}', {{ mqtt_port }});
}
// create icons
var icons = {
'rider_active_jo': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#FF0000;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}),
'rider_available': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#00FF00;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}),
'customer': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
})
};
var r_markers = {};
var c_markers = {};
var dashmap = initMap(r_markers, c_markers, icons);
initEventHandler(dashmap, icons);
</script>
{% endblock %} {% endblock %}

View file

@ -164,12 +164,34 @@
{% block scripts %} {% block scripts %}
<script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}" type="text/javascript"></script> <script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}&libraries=places" type="text/javascript"></script>
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script> <script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script> <script>
// BEGIN google maps stuff // BEGIN google maps stuff
// location search autocomplete
var input = document.getElementById('m_gmap_address');
var autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.setComponentRestrictions({'country': ['{% trans %}default_region{% endtrans %}']});
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place.geometry) {
return;
}
var message = {
'action': 'map.search',
'params': {
'lat': place.geometry.location.lat(),
'lng': place.geometry.location.lng()
}
};
console.log(message);
});
function selectPoint(map, latlng) { function selectPoint(map, latlng) {
var lat = latlng.lat(); var lat = latlng.lat();
var lng = latlng.lng(); var lng = latlng.lng();
@ -189,8 +211,8 @@ function selectPoint(map, latlng) {
var map = new GMaps({ var map = new GMaps({
div: '#m_gmap', div: '#m_gmap',
lat: 14.6091, lat: {% trans %}default_lat{% endtrans %},
lng: 121.0223, lng: {% trans %}default_long{% endtrans %},
click: function(e) { click: function(e) {
// handle click in map // handle click in map
selectPoint(map, e.latLng); selectPoint(map, e.latLng);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -893,53 +893,83 @@
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<!-- <script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}" type="text/javascript"></script> --> {{ include('map/' ~ map_js_file) }}
<script src="//maps.googleapis.com/maps/api/js?key={{ gmaps_api_key }}" type="text/javascript"></script> <script src="//maps.googleapis.com/maps/api/js?key={{ gmaps_api_key }}&libraries=places" type="text/javascript"></script>
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script> <script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script> <script>
// location search autocomplete
var input = document.getElementById('m_gmap_address');
var autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.setComponentRestrictions({'country': ['{% trans %}default_region{% endtrans %}']});
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place.geometry) {
return;
}
var message = {
'action': 'map.search',
'params': {
'lat': place.geometry.location.lat(),
'lng': place.geometry.location.lng()
}
};
console.log(message);
});
$(function() { $(function() {
var form_in_process = false; var form_in_process = false;
// BEGIN google maps stuff // openstreet maps stuff
function selectPoint(map, latlng) { // TODO: move this to a service
var lat = latlng.lat(); var default_lat = {% trans %}default_lat{% endtrans %};
var lng = latlng.lng(); var default_lng = {% trans %}default_long{% endtrans %};
// show it in map var osm_map = mapCreate('m_gmap', default_lat, default_lng, 'road', 13);
map.removeMarkers();
map.setCenter(lat, lng); var markerLayerGroup = L.layerGroup().addTo(osm_map);
map.addMarker({
lat: lat, function selectPoint(lat, lng)
lng: lng, {
icon: '/assets/images/icon-destination.png' // clear markers
}); markerLayerGroup.clearLayers();
var marker = L.marker([lat, lng]);
// add marker to layer group
markerLayerGroup.addLayer(marker);
osm_map.setView(new L.LatLng(lat, lng), 13);
// set value in hidden input // set value in hidden input
$('#map_lat').val(lat); $('#map_lat').val(lat);
$('#map_lng').val(lng); $('#map_lng').val(lng);
} }
var map = new GMaps({ osm_map.on('click', function(e) {
div: '#m_gmap', selectPoint(e.latlng.lat, e.latlng.lng);
lat: 14.6091,
lng: 121.0223,
click: function(e) {
// handle click in map
selectPoint(map, e.latLng);
e.stop();
}
}); });
var handleAction = function() { var handleAction = function() {
var text = $.trim($('#m_gmap_address').val()); var text = $.trim($('#m_gmap_address').val());
GMaps.geocode({ GMaps.geocode({
address: text, address: text,
callback: function(results, status) { callback: function(results, status) {
map.removeMarkers(); markerLayerGroup.clearLayers();
if (status == 'OK') { if (status == 'OK') {
selectPoint(map, results[0].geometry.location); console.log(results);
var lat = results[0].geometry.location.lat();
var lng = results[0].geometry.location.lng();
selectPoint(lat, lng);
} }
}, },
// TODO: change this to get what's in the translation file
// but for some strange reason, this worked.
region: 'ph' region: 'ph'
}); });
} }
@ -959,8 +989,10 @@ $(function() {
{% if ftags.set_map_coordinate %} {% if ftags.set_map_coordinate %}
// check if we need to set map // check if we need to set map
var latlng = new google.maps.LatLng({{ obj.getCoordinates.getLatitude }}, {{ obj.getCoordinates.getLongitude }}); // OSM code
selectPoint(map, latlng); var lat = {{ obj.getCoordinates.getLatitude }};
var lng = {{ obj.getCoordinates.getLongitude }};
selectPoint(lat, lng);
// remove placeholder text // remove placeholder text
$("[data-vehicle-field='1']").prop('placeholder', ''); $("[data-vehicle-field='1']").prop('placeholder', '');
@ -968,35 +1000,38 @@ $(function() {
{% if mode in ['update-processing', 'update-reassign-hub'] %} {% if mode in ['update-processing', 'update-reassign-hub'] %}
// display hub map // display hub map
var hmap = new GMaps({ // OSM code
div: '#hub_map',
lat: {{ obj.getCoordinates.getLatitude }}, var hub_lat = {{ obj.getCoordinates.getLatitude }};
lng: {{ obj.getCoordinates.getLongitude }} var hub_lng = {{ obj.getCoordinates.getLongitude }};
var osm_hmap = mapCreate('hub_map', hub_lat, hub_lng, 'road', 13);
var destLayerGroup = L.layerGroup().addTo(osm_hmap);
var hubLayerGroup = L.layerGroup().addTo(osm_hmap);
var dest_icon = L.icon({
iconUrl: '/assets/images/icon-destination.png',
shadowSize: [0,0]
}); });
hmap.addMarker({ var hub_icon = L.icon({
lat: {{ obj.getCoordinates.getLatitude }}, iconUrl: '/assets/images/icon-outlet.png',
lng: {{ obj.getCoordinates.getLongitude }}, shadowSize: [0,0]
icon: '/assets/images/icon-destination.png',
title: "Destination",
content: "Destination"
}); });
var dest_marker = new L.Marker([hub_lat, hub_lng], {icon: dest_icon}).bindPopup("Destination");
destLayerGroup.addLayer(dest_marker);
{% for hub in hubs %} {% for hub in hubs %}
hmap.addMarker({ var hub_marker = new L.Marker([{{ hub.hub.getCoordinates.getLatitude }},
lat: {{ hub.hub.getCoordinates.getLatitude }}, {{ hub.hub.getCoordinates.getLongitude }}], {icon: hub_icon}).bindPopup("{{ hub.hub.getName }}");
lng: {{ hub.hub.getCoordinates.getLongitude }},
title: "{{ hub.hub.getName }}", hubLayerGroup.addLayer(hub_marker);
content: "{{ hub.hub.getName }}",
icon: '/assets/images/icon-outlet.png'
});
{% endfor %} {% endfor %}
{% endif %} {% endif %}
// END google maps stuff
$("#row-form").submit(function(e) { $("#row-form").submit(function(e) {
if (form_in_process) { if (form_in_process) {
alert("Cannot submit form twice. First submission still in progress."); alert("Cannot submit form twice. First submission still in progress.");
@ -1443,7 +1478,7 @@ $(function() {
selectedHub = id; selectedHub = id;
// center the map // center the map
hmap.setCenter(lat, lng); osm_hmap.flyTo(L.latLng(lat, lng));
} else { } else {
// unhighlight this row // unhighlight this row
$(this).removeClass('m-table__row--primary'); $(this).removeClass('m-table__row--primary');
@ -1672,3 +1707,4 @@ $(function() {
}); });
</script> </script>
{% endblock %} {% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -221,7 +221,7 @@
<div class="form-group m-form__group row"> <div class="form-group m-form__group row">
<div class="col-lg-6"> <div class="col-lg-6">
<div class="col-lg-12 form-group-inner"> <div class="col-lg-12 form-group-inner">
<label data-field="delivery_instructions">Delivery Instructions</label> <label data-field="delivery_instructions">{% trans %}delivery_instructions_label{% endtrans %}</label>
<textarea name="delivery_instructions" class="form-control m-input" rows="4">{{ obj.getDeliveryInstructions }}</textarea> <textarea name="delivery_instructions" class="form-control m-input" rows="4">{{ obj.getDeliveryInstructions }}</textarea>
</div> </div>
</div> </div>
@ -651,8 +651,8 @@ $(function() {
var map = new GMaps({ var map = new GMaps({
div: '#m_gmap', div: '#m_gmap',
lat: 14.6091, lat: {% trans %}default_lat{% endtrans %},
lng: 121.0223, lng: {% trans %}default_long{% endtrans %},
click: function(e) { click: function(e) {
// handle click in map // handle click in map
selectPoint(map, e.latLng); selectPoint(map, e.latLng);

View file

@ -108,7 +108,7 @@
sortable: false, sortable: false,
overflow: 'visible', overflow: 'visible',
template: function (row, index, datatable) { template: function (row, index, datatable) {
var actions = '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" title="View / Edit"><i class="la la-edit"></i></a>'; var actions = '<a href="' + row.meta.update_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" title="View / Edit"><i class="la la-edit"></i></a>' + '<a href="' + row.meta.onestep_edit_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-edit" title="One Step Edit"><i class="la la-edit"></i></a>';
return actions; return actions;
}, },

View file

@ -122,6 +122,9 @@
{% if is_granted('jo_open.edit') %} {% if is_granted('jo_open.edit') %}
actions += '<a href="' + row.meta.edit_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-reassign-hub" title="Edit"><i class="fa fa-file"></i></a>'; actions += '<a href="' + row.meta.edit_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-reassign-hub" title="Edit"><i class="fa fa-file"></i></a>';
{% endif %} {% endif %}
{% if is_granted('jo_onestep.edit') %}
actions += '<a href="' + row.meta.onestep_edit_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-accent m-btn--icon m-btn--icon-only m-btn--pill btn-reassign-hub" title="One Step Edit"><i class="fa fa-file"></i></a>';
{% endif %}
return actions; return actions;
}, },

View file

@ -0,0 +1,13 @@
{% set cust = jo.getCustomer %}
{% set cv = jo.getCustomerVehicle %}
<strong>{{ cust.getNameDisplay }}</strong><br>
{{ cv.getPlateNumber }}<br>
<a href="{{ url('jo_onestep_edit_form', {'id': jo.getID}) }}">Job Order #{{ jo.getID }}</a><br>
{{ jo.getServiceTypeName }}<br>
{{ jo.getStatusText }}
{% if jo.getRider != null %}
<br><br>
{% set rider = jo.getRider %}
{{ rider.getFullName }}<br>
{{ rider.getPlateNumber }}
{% endif %}

View file

@ -0,0 +1,118 @@
{% extends 'base_minimal.html.twig' %}
{% block body %}
<div id="tracker_map" style="height:100%;"></div>
<div class="map-info">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="row">
<div class="col-12 d-flex flex-row justify-content-start">
<img class="mr-2 rider-image" src="{{ asset(rider.getImageFile ? "uploads/" ~ rider.getImageFile : "assets/images/user.gif") }}" alt="">
<div class="flex-grow-1">
<div><strong>Order #{{ jo.getID }}</strong></div>
<div>{{ rider.getFullName }}</div>
<div class="m-badge m-badge--brand m-badge--wide">{{ service_type }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<script src="{{ asset('assets/js/dashboard_map.js') }}"></script>
<script src="{{ asset('assets/js/map_mqtt.js') }}"></script>
{{ include('map/' ~ map_js_file) }}
<script>
function initMap(r_markers, c_markers, icons) {
var default_lat = {% trans %}default_lat{% endtrans %};
var default_lng = {% trans %}default_long{% endtrans %};
var options = {
'display_overlay': false,
'enable_popup': false,
'access_token': 'pk.eyJ1Ijoia2NvcmRlcm8iLCJhIjoiY2szbzA3ZHdsMDZxdTNsbGl4ZGNnN2VxaSJ9.LRzAe3RlV8sIP1N1x0chdw',
'div_id': 'tracker_map',
'center_lat': default_lat,
'center_lng': default_lng,
'map_type': 'road',
'zoom': 13,
'rider_popup_url': '/riders/[id]/popup',
'cust_popup_url': '/job-order/[id]/popup',
'icons': icons
};
var dashmap = new DashboardMap(options, r_markers, c_markers);
dashmap.initialize();
// dashmap.loadLocations('{{ path('rider_locations') }}');
return dashmap;
}
function initEventHandler(dashmap) {
var options = {
'track_jo': false,
'track_rider': true,
'channels': {
'rider_location': 'rider/{{ rider.getID }}/location',
'jo_location': 'none',
'jo_status': 'none'
},
};
var event_handler = new MapEventHandler(options, dashmap);
event_handler.connect('tracker', '{{ mqtt_host }}', {{ mqtt_port }});
}
// create icons
var icons = {
'rider_active_jo': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#FF0000;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}),
'rider_available': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#00FF00;' class='marker-pin'></div><i class='fa fa-bolt awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}),
'customer': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}),
'hub': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-home awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
})
};
var r_markers = {};
var c_markers = {};
var h_markers = {};
{% set hub = jo.getHub %}
var dashmap = initMap(r_markers, c_markers, icons);
dashmap.putCustomerMarker({{ jo.getID }}, {{ jo.getCoordinates.getLatitude }}, {{ jo.getCoordinates.getLongitude }});
dashmap.putRiderActiveJOMarker({{ rider.getID }}, {{ rider_pos.getLatitude }}, {{ rider_pos.getLongitude }});
dashmap.putMarker(
{{ hub.getID }},
{{ hub.getCoordinates.getLatitude }},
{{ hub.getCoordinates.getLongitude }},
h_markers,
icons['hub'],
dashmap.map,
'/'
);
initEventHandler(dashmap, icons);
</script>
{% endblock %}

View file

@ -0,0 +1,36 @@
<script type='text/javascript' src='https://www.bing.com/api/maps/mapcontrol?callback=initMap&key={{ bingmaps_api_key|raw }}' async defer></script>
<script>
function mapCreate(div_id, center_lat, center_lng, map_type, zoom) {
var map_type_id = Microsoft.Maps.MapTypeId.road;
switch (map_type) {
case 'road':
map_type_id = Microsoft.Maps.MapTypeId.road;
break;
case 'aerial':
map_type_id = Microsoft.Maps.MapTypeId.aerial;
break;
case 'dark':
map_type_id = Microsoft.Maps.MapTypeId.canvasDark;
break;
case 'light':
map_type_id = Microsoft.Maps.MapTypeId.canvasLight;
break;
case 'grayscale':
map_type_id = Microsoft.Maps.MapTypeId.grayscale;
break;
default:
map_type_id = Microsoft.Maps.MapTypeId.road;
break;
}
var map = new Microsoft.Maps.Map('#' + div_id, {
center: new Microsoft.Maps.Location(center_lat, center_lng),
mapTypeId: map_type_id,
zoom: zoom
});
return map;
}
</script>

View file

@ -0,0 +1,22 @@
<script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key|raw }}&callback=initMap" type="text/javascript" async defer></script>
<script>
function mapCreate(div_id, center_lat, center_lng, map_type, zoom) {
var map_type_id = 'roadmap';
switch (map_type) {
case 'road':
map_type_id = 'roadmap';
break;
default:
map_type_id = 'roadmap';
break;
}
var map = new google.maps.Map(document.getElementById(div_id),
{
center: {lat: center_lat, lng: center_lng},
mapTypeId: map_type_id,
zoom: zoom
});
}
</script>

View file

@ -0,0 +1,4 @@
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
crossorigin="">
</script>

View file

@ -0,0 +1,26 @@
<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
crossorigin="">
</script>
<script>
function mapCreate(div_id, center_lat, center_lng, map_type, zoom) {
var map = L.map(div_id).setView(
[center_lat, center_lng],
zoom
);
//TODO: put access token in .env
// add tile layer
var streets = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: 'Map data &copy; <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>',
maxZoom: 18,
id: 'mapbox/streets-v11',
accessToken: 'pk.eyJ1Ijoia2NvcmRlcm8iLCJhIjoiY2szbzA3ZHdsMDZxdTNsbGl4ZGNnN2VxaSJ9.LRzAe3RlV8sIP1N1x0chdw'
}).addTo(map);
return map;
}
</script>

View file

@ -143,12 +143,34 @@
{% block scripts %} {% block scripts %}
<script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}" type="text/javascript"></script> <script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}&libraries=places" type="text/javascript"></script>
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script> <script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script> <script>
// BEGIN google maps stuff // BEGIN google maps stuff
// location search autocomplete
var input = document.getElementById('m_gmap_address');
var autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.setComponentRestrictions({'country': ['{% trans %}default_region{% endtrans %}']});
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place.geometry) {
return;
}
var message = {
'action': 'map.search',
'params': {
'lat': place.geometry.location.lat(),
'lng': place.geometry.location.lng()
}
};
console.log(message);
});
function selectPoint(map, latlng) { function selectPoint(map, latlng) {
var lat = latlng.lat(); var lat = latlng.lat();
var lng = latlng.lng(); var lng = latlng.lng();
@ -168,8 +190,8 @@ function selectPoint(map, latlng) {
var map = new GMaps({ var map = new GMaps({
div: '#m_gmap', div: '#m_gmap',
lat: 14.6091, lat: {% trans %}default_lat{% endtrans %},
lng: 121.0223, lng: {% trans %}default_long{% endtrans %},
click: function(e) { click: function(e) {
// handle click in map // handle click in map
selectPoint(map, e.latLng); selectPoint(map, e.latLng);

View file

@ -174,12 +174,34 @@
{% block scripts %} {% block scripts %}
<script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}" type="text/javascript"></script> <script src="//maps.google.com/maps/api/js?key={{ gmaps_api_key }}&libraries=places" type="text/javascript"></script>
<script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script> <script src="/assets/vendors/custom/gmaps/gmaps.js" type="text/javascript"></script>
<script> <script>
// BEGIN google maps stuff // BEGIN google maps stuff
// location search autocomplete
var input = document.getElementById('m_gmap_address');
var autocomplete = new google.maps.places.Autocomplete(input);
autocomplete.setComponentRestrictions({'country': ['{% trans %}default_region{% endtrans %}']});
autocomplete.addListener('place_changed', function() {
var place = autocomplete.getPlace();
if (!place.geometry) {
return;
}
var message = {
'action': 'map.search',
'params': {
'lat': place.geometry.location.lat(),
'lng': place.geometry.location.lng()
}
};
console.log(message);
});
function selectPoint(map, latlng) { function selectPoint(map, latlng) {
var lat = latlng.lat(); var lat = latlng.lat();
var lng = latlng.lng(); var lng = latlng.lng();
@ -199,8 +221,8 @@ function selectPoint(map, latlng) {
var map = new GMaps({ var map = new GMaps({
div: '#m_gmap', div: '#m_gmap',
lat: 14.6091, lat: {% trans %}default_lat{% endtrans %},
lng: 121.0223, lng: {% trans %}default_long{% endtrans %},
click: function(e) { click: function(e) {
// handle click in map // handle click in map
selectPoint(map, e.latLng); selectPoint(map, e.latLng);

View file

@ -0,0 +1,13 @@
<strong>{{ rider.getFullName }}</strong>
{% set jo = rider.getActiveJobOrder %}
{% if jo is not null %}
{% set cust = jo.getCustomer %}
{% set cv = jo.getCustomerVehicle %}
<br>
<a href="{{ url('jo_onestep_edit_form', {'id': jo.getID}) }}">Job Order #{{ jo.getID }}</a><br>
{{ jo.getServiceTypeName }}<br>
{{ jo.getStatusText }}<br><br>
{{ cust.getNameDisplay }}<br>
{{ cv.getPlateNumber }}
{% endif %}

View file

@ -0,0 +1,27 @@
# text
title_login: Res-Q for CMB | Login
block_title: Res-Q for CMB
control_panel_sign_in: Sign-in to Control Panel
alt_image_logo_login: Res-Q for CMB
alt_image_dashboard: Res-Q for CMB
copyright: Res-Q for CMB
battery_size_tradein_brand: Trade-in Motolite
battery_size_tradein_premium: Trade-in Premium
battery_size_tradein_other: Trade-in Other
add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Res-Q for CMB Job Order
country_code_prefix: '+60'
delivery_instructions_label: 'Delivery Instructions - CarFix Job Order No.'
# images
image_logo_login: /assets/images/black-text-logo-01.png
icon_login: /assets/images/battery-assist-bm-logo-32x32.png
icon_base_32x32: /assets/images/black-text-logo-01-32x32.png
icon_base_16x16: /assets/images/black-text-logo-01-16x16.png
image_dashboard: /assets/images/century_logo.png
image_jo_pdf: /public/assets/images/black-text-logo-01-115x115.png
# default point for maps
default_lat: 3.084216
default_long: 101.6129996
default_region: my

View file

@ -1,21 +1,29 @@
# text # text
title_login: Motolite Res-Q | Login title_login: Res-Q for CMB | Login
block_title: Motolite Res-Q block_title: Res-Q for CMB
control_panel_sign_in: Sign-in to Control Panel control_panel_sign_in: Sign-in to Control Panel
alt_image_logo_login: Res-Q alt_image_logo_login: Res-Q for CMB
alt_image_dashboard: Motolite alt_image_dashboard: Res-Q for CMB
copyright: Motolite Res-Q copyright: Res-Q for CMB
battery_size_tradein_brand: Trade-in Motolite battery_size_tradein_brand: Trade-in Motolite
battery_size_tradein_premium: Trade-in Premium battery_size_tradein_premium: Trade-in Premium
battery_size_tradein_other: Trade-in Other battery_size_tradein_other: Trade-in Other
add_cust_vehicle_battery_info: This vehicle is using a Motolite battery add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Motolite Res-Q Job Order jo_title_pdf: Res-Q for CMB Job Order
country_code_prefix: '+63' country_code_prefix: '+60'
delivery_instructions_label: 'Delivery Instructions - CarFix Job Order No.'
# # images # images
image_logo_login: /assets/images/logo-resq.png image_logo_login: /assets/images/black-text-logo-01.png
icon_login: /assets/demo/default/media/img/logo/favicon.ico icon_login: /assets/images/battery-assist-bm-logo-32x32.png
icon_base_32x32: /assets/images/favicon/favicon-32x32.png icon_base_32x32: /assets/images/black-text-logo-01-32x32.png
icon_base_16x16: /assets/images/favicon/favicon-16x16.png icon_base_16x16: /assets/images/black-text-logo-01-16x16.png
image_dashboard: /assets/images/logo-motolite.png image_dashboard: /assets/images/century_logo.png
image_jo_pdf: /public/assets/images/logo-resq.png image_jo_pdf: /public/assets/images/black-text-logo-01-115x115.png
# default point for maps
default_lat: 14.6091
default_long: 121.0223
#default_lat: 3.084216
#default_long: 101.6129996
default_region: ph

View file

@ -11,6 +11,7 @@ battery_size_tradein_other: Trade-in Other
add_cust_vehicle_battery_info: This vehicle is using a Motolite battery add_cust_vehicle_battery_info: This vehicle is using a Motolite battery
jo_title_pdf: Motolite Res-Q Job Order jo_title_pdf: Motolite Res-Q Job Order
country_code_prefix: '+63' country_code_prefix: '+63'
delivery_instructions_label: Delivery Instructions
# images # images
image_logo_login: /assets/images/logo-resq.png image_logo_login: /assets/images/logo-resq.png
@ -19,3 +20,8 @@ icon_base_32x32: /assets/images/favicon/favicon-32x32.png
icon_base_16x16: /assets/images/favicon/favicon-16x16.png icon_base_16x16: /assets/images/favicon/favicon-16x16.png
image_dashboard: /assets/images/logo-motolite.png image_dashboard: /assets/images/logo-motolite.png
image_jo_pdf: /public/assets/images/logo-resq.png image_jo_pdf: /public/assets/images/logo-resq.png
# default point for maps
default_lat: 14.6091
default_long: 121.0223
default_region: ph

View file

@ -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])

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more