Merge branch '270-final-cmb-fixes' into 'master'

Resolve "Final CMB Fixes"

Closes #270

See merge request jankstudio/resq!418
This commit is contained in:
Kendrick Chan 2020-04-03 02:54:28 +00:00
commit e3cb53cb78
45 changed files with 4141 additions and 259 deletions

View file

@ -215,6 +215,20 @@ access_keys:
- id: rider.delete
label: Delete
- id: servicecharge
label: Service Charge
acls:
- id: service_charge.menu
label: Menu
- id: service_charge.list
label: List
- id: service_charge.add
label: Add
- id: service_charge.update
label: Update
- id: service_charge.delete
label: Delete
- id: joborder
label: Job Order
acls:
@ -246,6 +260,10 @@ access_keys:
label: One-step Process
- id: jo_onestep.edit
label: One-step Process Edit
- id: jo_walkin.form
label: Walk-in
- id: jo_walkin.edit
label: Walk-in Edit
- id: support
label: Customer Support Access

179
config/cmb.menu.yaml Normal file
View file

@ -0,0 +1,179 @@
main_menu:
- id: home
acl: dashboard.menu
label: Dashboard
icon: flaticon-line-graph
- id: user
acl: user.menu
label: User
icon: flaticon-users
- id: user_list
acl: user.list
label: Users
parent: user
- id: role_list
acl: role.list
label: Roles
parent: user
- id: apiuser
acl: apiuser.menu
label: API User
icon: flaticon-users
- id: api_user_list
acl: apiuser.list
label: API Users
parent: apiuser
- id: api_role_list
acl: apirole.list
label: API Roles
parent: apiuser
- id: logistics
acl: logistics.menu
label: Logistics
icon: fa fa-truck
- id: rider_list
acl: rider.list
label: Riders
parent: logistics
- id: service_charge_list
acl: service_charge.list
label: Service Charges
parent: logistics
- id: battery
acl: battery.menu
label: Battery
icon: fa fa-battery-3
- id: battery_list
acl: battery.list
label: Batteries
parent: battery
- id: bmfg_list
acl: bmfg.list
label: Manufacturers
parent: battery
- id: bmodel_list
acl: bmodel.list
label: Models
parent: battery
- id: bsize_list
acl: bsize.list
label: Sizes
parent: battery
- id: promo_list
acl: promo.list
label: Promos
parent: battery
- id: vehicle
acl: vehicle.menu
label: Vehicle
icon: fa fa-car
- id: vehicle_list
acl: vehicle.list
label: Vehicles
parent: vehicle
- id: vmfg_list
acl: vmfg.list
label: Manufacturers
parent: vehicle
- id: location
acl: location.menu
label: Location
icon: fa fa-home
- id: outlet_list
acl: outlet.menu
label: Outlet
parent: location
- id: hub_list
acl: hub.menu
label: Hub
parent: location
- id: geofence_list
acl: geofence.menu
label: Geofence
parent: location
- id: joborder
acl: joborder.menu
label: Job Order
icon: flaticon-calendar-3
- id: jo_onestep_form
acl: jo_onestep.form
label: One-step Process
parent: joborder
- id: jo_walkin_form
acl: jo_walkin.form
label: Walk-in
parent: joborder
- id: jo_open
acl: jo_open.list
label: Open
parent: joborder
- id: jo_all
acl: jo_all.list
label: View All
parent: joborder
- id: support
acl: support.menu
label: Customer Support
icon: flaticon-support
- id: customer_list
acl: customer.list
label: Customers
parent: support
- id: ticket_list
acl: ticket.list
label: Tickets
parent: support
- id: general_search
acl: general.search
label: Search
parent: support
- id: warranty_search
acl: warranty.search
label: Customer Battery Search
parent: support
- id: privacy_policy_list
acl: privacy_policy.list
label: Privacy Policy
parent: support
- id: warranty_list
acl: warranty.list
label: Warranty
parent: support
- id: warranty_upload
acl: warranty.upload
label: Warranty Upload
parent: support
- id: static_content_list
acl: static_content.list
label: Static Content
parent: support
- id: service
acl: service.menu
label: Other Services
icon: flaticon-squares
- id: service_list
acl: service.list
label: Services
parent: service
- id: partner
acl: partner.menu
label: Partners
icon: flaticon-network
- id: partner_list
acl: partner.list
label: Partners
parent: partner
- id: review_list
acl: review.list
label: Reviews
parent: partner

View file

@ -37,6 +37,10 @@ main_menu:
acl: rider.list
label: Riders
parent: logistics
- id: service_charge_list
acl: service_charge.list
label: Service Charges
parent: logistics
- id: battery
acl: battery.menu
@ -102,21 +106,9 @@ main_menu:
acl: jo_onestep.form
label: One-step Process
parent: joborder
- id: jo_in
acl: jo_in.list
label: Incoming
parent: joborder
- id: jo_proc
acl: jo_proc.list
label: Dispatch
parent: joborder
- id: jo_assign
acl: jo_assign.list
label: Rider Assignment
parent: joborder
- id: jo_fulfill
acl: jo_fulfill.list
label: Fulfillment
- id: jo_walkin_form
acl: jo_walkin.form
label: Walk-in
parent: joborder
- id: jo_open
acl: jo_open.list

183
config/resq.menu.yaml Normal file
View file

@ -0,0 +1,183 @@
main_menu:
- id: home
acl: dashboard.menu
label: Dashboard
icon: flaticon-line-graph
- id: user
acl: user.menu
label: User
icon: flaticon-users
- id: user_list
acl: user.list
label: Users
parent: user
- id: role_list
acl: role.list
label: Roles
parent: user
- id: apiuser
acl: apiuser.menu
label: API User
icon: flaticon-users
- id: api_user_list
acl: apiuser.list
label: API Users
parent: apiuser
- id: api_role_list
acl: apirole.list
label: API Roles
parent: apiuser
- id: logistics
acl: logistics.menu
label: Logistics
icon: fa fa-truck
- id: rider_list
acl: rider.list
label: Riders
parent: logistics
- id: battery
acl: battery.menu
label: Battery
icon: fa fa-battery-3
- id: battery_list
acl: battery.list
label: Batteries
parent: battery
- id: bmfg_list
acl: bmfg.list
label: Manufacturers
parent: battery
- id: bmodel_list
acl: bmodel.list
label: Models
parent: battery
- id: bsize_list
acl: bsize.list
label: Sizes
parent: battery
- id: promo_list
acl: promo.list
label: Promos
parent: battery
- id: vehicle
acl: vehicle.menu
label: Vehicle
icon: fa fa-car
- id: vehicle_list
acl: vehicle.list
label: Vehicles
parent: vehicle
- id: vmfg_list
acl: vmfg.list
label: Manufacturers
parent: vehicle
- id: location
acl: location.menu
label: Location
icon: fa fa-home
- id: outlet_list
acl: outlet.menu
label: Outlet
parent: location
- id: hub_list
acl: hub.menu
label: Hub
parent: location
- id: geofence_list
acl: geofence.menu
label: Geofence
parent: location
- id: joborder
acl: joborder.menu
label: Job Order
icon: flaticon-calendar-3
- id: jo_in
acl: jo_in.list
label: Incoming
parent: joborder
- id: jo_proc
acl: jo_proc.list
label: Dispatch
parent: joborder
- id: jo_assign
acl: jo_assign.list
label: Rider Assignment
parent: joborder
- id: jo_fulfill
acl: jo_fulfill.list
label: Fulfillment
parent: joborder
- id: jo_open
acl: jo_open.list
label: Open
parent: joborder
- id: jo_all
acl: jo_all.list
label: View All
parent: joborder
- id: support
acl: support.menu
label: Customer Support
icon: flaticon-support
- id: customer_list
acl: customer.list
label: Customers
parent: support
- id: ticket_list
acl: ticket.list
label: Tickets
parent: support
- id: general_search
acl: general.search
label: Search
parent: support
- id: warranty_search
acl: warranty.search
label: Customer Battery Search
parent: support
- id: privacy_policy_list
acl: privacy_policy.list
label: Privacy Policy
parent: support
- id: warranty_list
acl: warranty.list
label: Warranty
parent: support
- id: warranty_upload
acl: warranty.upload
label: Warranty Upload
parent: support
- id: static_content_list
acl: static_content.list
label: Static Content
parent: support
- id: service
acl: service.menu
label: Other Services
icon: flaticon-squares
- id: service_list
acl: service.list
label: Services
parent: service
- id: partner
acl: partner.menu
label: Partners
icon: flaticon-network
- id: partner_list
acl: partner.list
label: Partners
parent: partner
- id: review_list
acl: review.list
label: Reviews
parent: partner

View file

@ -206,3 +206,23 @@ jo_tracker:
controller: App\Controller\JobOrderController::tracker
methods: [GET]
jo_walkin_form:
path: /job-order/walk-in
controller: App\Controller\JobOrderController::walkInForm
methods: [GET]
jo_walkin_submit:
path: /job-order/walk-in
controller: App\Controller\JobOrderController::walkInSubmit
methods: [POST]
jo_walkin_edit_form:
path: /job-order/walk-in/{id}
controller: App\Controller\JobOrderController::walkInEditForm
methods: [GET]
jo_walkin_edit_submit:
path: /job-order/walk-in/{id}
controller: App\Controller\JobOrderController::walkInEditSubmit
methods: [POST]

View file

@ -41,3 +41,18 @@ rider_ajax_popup:
path: /riders/{id}/popup
controller: App\Controller\RiderController::popupInfo
methods: [GET]
rider_active_jo:
path: /riders/{id}/activejo/{jo_id}
controller: App\Controller\RiderController::riderActiveJO
methods: [GET]
rider_priority_up_jo:
path: /riders/{id}/priority_up/{jo_id}
controller: App\Controller\RiderController::priorityUpJO
methods: [GET]
rider_priority_down_jo:
path: /riders/{id}/priority_down/{jo_id}
controller: App\Controller\RiderController::priorityDownJO
methods: [GET]

View file

@ -0,0 +1,34 @@
service_charge_list:
path: /service_charges
controller: App\Controller\ServiceChargeController::index
service_charge_rows:
path: /service_charges/rows
controller: App\Controller\ServiceChargeController::rows
methods: [POST]
service_charge_create:
path: /service_charges/create
controller: App\Controller\ServiceChargeController::addForm
methods: [GET]
service_charge_create_submit:
path: /service_charges/create
controller: App\Controller\ServiceChargeController::addSubmit
methods: [POST]
service_charge_update:
path: /service_charges/{id}
controller: App\Controller\ServiceChargeController::updateForm
methods: [GET]
service_charge_update_submit:
path: /service_charges/{id}
controller: App\Controller\ServiceChargeController::updateSubmit
methods: [POST]
service_charge_delete:
path: /service_charges/{id}
controller: App\Controller\ServiceChargeController::destroy
methods: [DELETE]

View file

@ -0,0 +1 @@
INSERT INTO `service_charge` VALUES(1,'Bangi',20),(2,'Banting',30),(3,'Bdr Saujana Utama',20),(4,'Bdr Seri Coalfields',30),(5,'Bdr Baru Bangi',20),(6,'Bdr Saujana Putra',20),(7,'Bukit Beruntung',30),(8,'Cyberjaya',20),(9,'Dengkil',30),(10,'Hulu Langat',20),(11,'Jenjarom',30),(12,'Klia',30),(13,'Meru',20),(14,'Port Klang',20),(15,'Pulau Indah',30),(16,'Puncak Alam',20),(17,'Putrajaya',20),(18,'Rawang',30),(19,'Salak Tinggi',30),(20,'Semenyih',20),(21,'Sepang',30),(22,'Serendah',30),(23,'Sungai Buloh',20),(24,'Teluk Panglima Garang',30),(25,'Uitm Puncak Alam',20),(26,'12am - 7am',10),(27,'Out of define Klg Valley',20),(28,'Airport',35),(29,'Jump start',50),(30,'Product warranty service charge - existing BA customer',20),(31,'Product warranty service charge - non BA customer',40);

View file

@ -51,6 +51,32 @@ class DashboardMap {
return this.map;
}
switchRiderStatus(rider_id, rider_status) {
console.log('switching rider ' + rider_id + ' to ' + rider_status);
// find the marker
console.log(this.rider_markers);
if (this.rider_markers.hasOwnProperty(rider_id)) {
var marker = this.rider_markers[rider_id];
} else {
// TODO: call ajax to get location and create marker
console.log('marker not found for rider');
return true;
}
// add it to proper layer group
console.log(rider_status);
if (rider_status == 'available') {
this.layer_groups.rider_active_jo.removeLayer(marker);
this.layer_groups.rider_available.addLayer(marker);
marker.setIcon(this.options.icons.rider_available);
} else if (rider_status == 'jo') {
this.layer_groups.rider_available.removeLayer(marker);
this.layer_groups.rider_active_jo.addLayer(marker);
marker.setIcon(this.options.icons.rider_active_jo);
}
}
putMarker(id, lat, lng, markers, icon, layer_group, popup_url) {
var my = this;
// existing marker
@ -131,6 +157,19 @@ class DashboardMap {
);
}
removeRiderMarker(id) {
console.log('removing rider marker for ' + id);
var markers = this.rider_markers;
if (!markers.hasOwnProperty(id)) {
console.log('no such marker to remove');
return;
}
this.layer_groups.rider_active_jo.removeLayer(markers[id]);
this.layer_groups.rider_available.removeLayer(markers[id]);
}
loadLocations(location_url) {
console.log(this.rider_markers);
var my = this;

View file

@ -27,18 +27,26 @@ class MapEventHandler {
console.log('mqtt connected!');
var my = icontext.invocationContext;
// subscribe to rider locations
if (my.options.track_rider) {
// subscribe to rider locations
console.log('subscribing to ' + my.options.channels.rider_location);
my.mqtt.subscribe(my.options.channels.rider_location);
// subscribe to rider status
console.log('subscribing to ' + my.options.channels.rider_status);
my.mqtt.subscribe(my.options.channels.rider_status);
}
// subscribe to jo locations
if (my.options.track_jo) {
// subscribe to jo locations
console.log('subscribing to ' + my.options.channels.jo_location);
my.mqtt.subscribe(my.options.channels.jo_location);
// subscribe to jo status
console.log('subscribing to ' + my.options.channels.jo_status);
my.mqtt.subscribe(my.options.channels.jo_status);
}
}
onMessage(msg) {
@ -75,8 +83,24 @@ class MapEventHandler {
var lat = parseFloat(pl_split[0]);
var lng = parseFloat(pl_split[1]);
// TODO: check if available or not
this.dashmap.putRiderAvailableMarker(chan_split[1], lat, lng);
break;
case "status":
console.log("got status for rider " + chan_split[1] + " - " + payload);
switch (payload) {
case 'available':
this.dashmap.switchRiderStatus(chan_split[1], 'available');
break;
case 'jo':
console.log('jo status');
this.dashmap.switchRiderStatus(chan_split[1], 'jo');
break;
case 'logout':
this.dashmap.removeRiderMarker(chan_split[1]);
break;
}
break;
}
}
@ -102,6 +126,7 @@ class MapEventHandler {
this.dashmap.putCustomerMarker(id, lat, lng);
break;
case "status":
console.log("got status for jo " + payload);
switch (payload) {
case 'cancel':
case 'fulfill':

View file

@ -181,7 +181,8 @@ class CustomerController extends Controller
public function getCustomerVehicles(Request $req, CustomerHandlerInterface $cust_handler)
{
if (!$this->isGranted('jo_in.list')) {
if ((!$this->isGranted('jo_onestep.form')) ||
(!$this->isGranted('jo_walkin.form'))) {
$exception = $this->createAccessDeniedException('No access.');
throw $exception;
}

View file

@ -12,6 +12,7 @@ use App\Entity\Battery;
use App\Entity\JobOrder;
use App\Entity\VehicleManufacturer;
use App\Entity\Vehicle;
use App\Entity\Hub;
use App\Service\InvoiceGeneratorInterface;
use App\Service\JobOrderHandlerInterface;
@ -275,12 +276,14 @@ class JobOrderController extends Controller
{
$rows[$key]['meta']['reassign_hub_url'] = $this->generateUrl('jo_open_hub_form', ['id' => $jo_id]);
$rows[$key]['meta']['reassign_rider_url'] = $this->generateUrl('jo_open_rider_form', ['id' => $jo_id]);
$rows[$key]['meta']['edit_url'] = $this->generateUrl('jo_open_edit_form', ['id' => $jo_id]);
// $rows[$key]['meta']['edit_url'] = $this->generateUrl('jo_open_edit_form', ['id' => $jo_id]);
$rows[$key]['meta']['edit_url'] = $this->generateUrl($jo_handler->getEditRoute($jo_id, $tier_params['edit_route']), ['id' => $jo_id]);
$rows[$key]['meta']['onestep_edit_url'] = $this->generateUrl('jo_onestep_edit_form', ['id' => $jo_id]);
}
else
{
$rows[$key]['meta']['update_url'] = $this->generateUrl($tier_params['edit_route'], ['id' => $jo_id]);
// $rows[$key]['meta']['update_url'] = $this->generateUrl($tier_params['edit_route'], ['id' => $jo_id]);
$rows[$key]['meta']['update_url'] = $this->generateUrl($jo_handler->getEditRoute($jo_id, $tier_params['edit_route']), ['id' => $jo_id]);
$rows[$key]['meta']['onestep_edit_url'] = $this->generateUrl('jo_onestep_edit_form', ['id' => $jo_id]);
$rows[$key]['meta']['pdf_url'] = $this->generateUrl('jo_pdf_form', ['id' => $jo_id]);
}
@ -429,7 +432,7 @@ class JobOrderController extends Controller
* @Menu(selected="jo_fulfill")
*/
public function fulfillmentForm(JobOrderHandlerInterface $jo_handler, $id,
GISManagerInterface $gis)
GISManagerInterface $gis, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('jo_fulfill.list', null, 'No access.');
@ -442,6 +445,8 @@ class JobOrderController extends Controller
throw $this->createNotFoundException($e->getMessage());
}
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['vmakes'] = $em->getRepository(Vehicle::class)->findAll();
$params['submit_url'] = $this->generateUrl('jo_fulfill_submit', ['id' => $id]);
$params['return_url'] = $this->generateUrl('jo_fulfill');
$params['map_js_file'] = $gis->getJSJOFile();
@ -604,7 +609,7 @@ class JobOrderController extends Controller
* @Menu(selected="jo_all")
*/
public function allForm($id, JobOrderHandlerInterface $jo_handler,
GISManagerInterface $gis)
GISManagerInterface $gis, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('jo_all.list', null, 'No access.');
@ -617,6 +622,8 @@ class JobOrderController extends Controller
throw $this->createNotFoundException($e->getMessage());
}
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['vmakes'] = $em->getRepository(Vehicle::class)->findAll();
$params['return_url'] = $this->generateUrl('jo_all');
$params['submit_url'] = '';
$params['map_js_file'] = $gis->getJSJOFile();
@ -691,6 +698,7 @@ class JobOrderController extends Controller
$items = $req->request->get('items');
$promo_id = $req->request->get('promo');
$cvid = $req->request->get('cvid');
$service_charges = $req->request->get('service_charges', []);
$em = $this->getDoctrine()->getManager();
@ -734,11 +742,7 @@ class JobOrderController extends Controller
}
*/
// TODO: this snippet should be in the invoice generator
$error = $ic->invoicePromo($criteria, $promo_id);
if (!$error)
$error = $ic->invoiceBatteries($criteria, $items);
$error = $ic->generateDraftInvoice($criteria, $promo_id, $service_charges, $items);
if ($error)
{
@ -880,7 +884,6 @@ class JobOrderController extends Controller
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
@ -962,4 +965,93 @@ class JobOrderController extends Controller
return $this->render('job-order/tracker.html.twig', $params);
}
/**
* @Menu(selected="jo_walkin_form")
*/
public function walkInForm(EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler)
{
$this->denyAccessUnlessGranted('jo_walkin.form', null, 'No access.');
$params = $jo_handler->initializeWalkinForm();
$params['submit_url'] = $this->generateUrl('jo_walkin_submit');
$params['return_url'] = $this->generateUrl('jo_walkin_form');
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['vmakes'] = $em->getRepository(Vehicle::class)->findAll();
$params['hubs'] = $em->getRepository(Hub::class)->findAll();
$template = $params['template'];
// response
return $this->render($template, $params);
}
public function walkInSubmit(Request $req, JobOrderHandlerInterface $jo_handler)
{
$this->denyAccessUnlessGranted('jo_walkin.form', null, 'No access.');
// initialize error list
$error_array = [];
$id = -1;
$error_array = $jo_handler->processWalkinJobOrder($req, $id);
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
/**
* @Menu(selected="jo_walkin_edit_form")
*/
public function walkInEditForm($id, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler)
{
$this->denyAccessUnlessGranted('jo_walkin.edit', null, 'No access.');
$params = $jo_handler->initializeWalkinEditForm($id);
$params['submit_url'] = $this->generateUrl('jo_walkin_edit_submit', ['id' => $id]);
$params['return_url'] = $this->generateUrl('jo_open');
$params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll();
$params['vmakes'] = $em->getRepository(Vehicle::class)->findAll();
$params['hubs'] = $em->getRepository(Hub::class)->findAll();
$template = $params['template'];
// response
return $this->render($template, $params);
}
public function walkInEditSubmit(Request $req, JobOrderHandlerInterface $jo_handler, $id)
{
$this->denyAccessUnlessGranted('jo_walkin.edit', null, 'No access.');
$error_array = [];
$error_array = $jo_handler->processWalkinJobOrder($req, $id);
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
}
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
}

View file

@ -7,7 +7,10 @@ use App\Entity\Rider;
use App\Entity\RiderSchedule;
use App\Entity\Hub;
use App\Entity\User;
use App\Entity\JobOrder;
use App\Service\FileUploader;
use App\Service\MQTTClient;
use Doctrine\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
@ -18,6 +21,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Catalyst\MenuBundle\Annotation\Menu;
use DateTime;
@ -510,4 +515,85 @@ class RiderController extends Controller
return $this->render('rider/popup.html.twig', [ 'rider' => $rider ]);
}
/**
* @ParamConverter("rider", class="App\Entity\Rider")
*/
public function riderActiveJO(EntityManagerInterface $em, MQTTClient $mclient, Rider $rider, $jo_id)
{
$jo = $em->getRepository(JobOrder::class)->find($jo_id);
$rider->setActiveJobOrder($jo);
$em->flush();
// TODO: trigger what needs triggering in rider app
$payload = [
'event' => 'cancelled',
'reason' => 'Reprioritization',
'jo_id' => $jo->getID(),
];
$mclient->sendRiderEvent($jo, $payload);
return $this->redirecttoRoute('rider_update', ['id' => $rider->getID()]);
}
/**
* @ParamConverter("rider", class="App\Entity\Rider")
* @ParamConverter("jo", class="App\Entity\JobOrder", options={"id": "jo_id"})
*/
public function priorityUpJO(EntityManagerInterface $em, Rider $rider, JobOrder $jo)
{
$jos = $rider->getOpenJobOrders();
// set new priority
$old_prio = $jo->getPriority();
$new_prio = $old_prio - 1;
$jo->setPriority($new_prio);
// go through all rider open JOs and set priority when needed
foreach ($jos as $rider_jo)
{
// check if it's the same
if ($rider_jo->getID() == $jo->getID())
continue;
// if priority is the same as old priority, move it down
if ($new_prio == $rider_jo->getPriority())
$rider_jo->setPriority($rider_jo->getPriority() + 1);
}
$em->flush();
return $this->redirecttoRoute('rider_update', ['id' => $rider->getID()]);
}
/**
* @ParamConverter("rider", class="App\Entity\Rider")
* @ParamConverter("jo", class="App\Entity\JobOrder", options={"id": "jo_id"})
*/
public function priorityDownJO(EntityManagerInterface $em, Rider $rider, JobOrder $jo)
{
$jos = $rider->getOpenJobOrders();
// set new priority
$old_prio = $jo->getPriority();
$new_prio = $old_prio + 1;
$jo->setPriority($new_prio);
// go through all rider open JOs and set priority when needed
foreach ($jos as $rider_jo)
{
// check if it's the same
if ($rider_jo->getID() == $jo->getID())
continue;
// if priority is the same as old priority, move it down
if ($new_prio == $rider_jo->getPriority())
$rider_jo->setPriority($rider_jo->getPriority() - 1);
}
$em->flush();
return $this->redirecttoRoute('rider_update', ['id' => $rider->getID()]);
}
}

View file

@ -248,7 +248,7 @@ class RoleController extends Controller
if (!$row->isSuperAdmin())
{
// clear first
$row->clearACLAttributes();
$row->clearACLAccess();
// then add
$acl_attribs = $req->request->get('acl');

View file

@ -0,0 +1,268 @@
<?php
namespace App\Controller;
use App\Entity\ServiceCharge;
use Doctrine\ORM\Query;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Catalyst\MenuBundle\Annotation\Menu;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class ServiceChargeController extends Controller
{
/**
* @Menu(selected="service_charge_list")
*/
public function index()
{
$this->denyAccessUnlessGranted('service_charge.list', null, 'No access.');
return $this->render('service-charge/list.html.twig');
}
public function rows(Request $req)
{
$this->denyAccessUnlessGranted('service_charge.list', null, 'No access.');
// build query
$qb = $this->getDoctrine()
->getRepository(ServiceCharge::class)
->createQueryBuilder('q');
// get datatable params
$datatable = $req->request->get('datatable');
// count total records
$tquery = $qb->select('COUNT(q)');
// add fitlers 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.id', '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['name'] = $orow->getName();
// add row metadata
$row['meta'] = [
'update_url' => '',
'delete_url' => ''
];
// add crud urls
if ($this->isGranted('service_charge.update'))
$row['meta']['update_url'] = $this->generateUrl('service_charge_update', ['id' => $row['id']]);
if ($this->isGranted('service.delete'))
$row['meta']['delete_url'] = $this->generateUrl('service_charge_delete', ['id' => $row['id']]);
$rows[] = $row;
}
// response
return $this->json([
'meta' => $meta,
'data' => $rows
]);
}
/**
* @Menu(selected="service_charge_list")
*/
public function addForm()
{
$this->denyAccessUnlessGranted('service_charge.add', null, 'No access.');
$params = [];
$params['obj'] = new ServiceCharge();
$params['mode'] = 'create';
// response
return $this->render('service-charge/form.html.twig', $params);
}
public function addSubmit(Request $req, ValidatorInterface $validator, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('service_charge.add', null, 'No access.');
// create new object
$row = new ServiceCharge();
// set and save values
$row->setName($req->request->get('name'));
$row->setAmount($req->request->get('amount'));
// validate
$errors = $validator->validate($row);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
} else {
// validated! save the entity
$em->persist($row);
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
}
/**
* @Menu(selected="service_charge_list")
* @ParamConverter("sc", class="App\Entity\ServiceCharge")
*/
public function updateForm(ServiceCharge $sc)
{
$this->denyAccessUnlessGranted('service_charge.update', null, 'No access.');
$params = [];
$params['mode'] = 'update';
if ($sc == null)
throw $this->createNotFoundException('The item does not exist');
$params['obj'] = $sc;
// response
return $this->render('service-charge/form.html.twig', $params);
}
/**
* @ParamConverter("sc", class="App\Entity\ServiceCharge")
*/
public function updateSubmit(Request $req, ValidatorInterface $validator,
ServiceCharge $sc, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('service_charge.update', null, 'No access.');
// make sure this row exists
if ($sc == null)
throw $this->createNotFoundException('The item does not exist');
// set and save values
$sc->setName($req->request->get('name'));
$sc->setAmount($req->request->get('amount'));
// validate
$errors = $validator->validate($sc);
// initialize error list
$error_array = [];
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if any errors were found
if (!empty($error_array)) {
// return validation failure response
return $this->json([
'success' => false,
'errors' => $error_array
], 422);
} else {
// validated! save the entity
$em->flush();
// return successful response
return $this->json([
'success' => 'Changes have been saved!'
]);
}
}
/**
* @Menu(selected="service_list")
* @ParamConverter("sc", class="App\Entity\ServiceCharge")
*/
public function destroy(ServiceCharge $sc, EntityManagerInterface $em)
{
$this->denyAccessUnlessGranted('service_charge.delete', null, 'No access.');
$params = [];
if ($sc == null)
throw $this->createNotFoundException('The item does not exist');
// delete this row
$em->remove($sc);
$em->flush();
// response
$response = new Response();
$response->setStatusCode(Response::HTTP_OK);
$response->send();
}
protected function setQueryFilters($datatable, &$query)
{
if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) {
$query->where('q.name LIKE :filter')
->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%');
}
}
}

View file

@ -280,6 +280,20 @@ class JobOrder
*/
protected $hub_rejections;
// priority order for riders
// NOTE: this is a workaround since changeing rider to jo rider assignment with details requires
// too many changes and may break too many things.
/**
* @ORM\Column(type="integer", options={"default": 0}))
*/
protected $priority;
// meta
/**
* @ORM\Column(type="json")
*/
protected $meta;
public function __construct()
{
$this->date_create = new DateTime();
@ -297,6 +311,9 @@ class JobOrder
$this->trade_in_type = null;
$this->flag_rider_rating = false;
$this->flag_coolant = false;
$this->priority = 0;
$this->meta = [];
}
public function getID()
@ -802,4 +819,30 @@ class JobOrder
{
return $this->hub_rejections;
}
public function setPriority($priority)
{
$this->priority = $priority;
return $this;
}
public function getPriority()
{
return $this->priority;
}
public function addMeta($id, $value)
{
$this->meta[$id] = $value;
return $this;
}
public function getMeta($id)
{
// return null if we don't have it
if (!isset($this->meta[$id]))
return null;
return $this->meta[$id];
}
}

View file

@ -61,9 +61,17 @@ class Rider
// job orders that the rider has done
/**
* @ORM\OneToMany(targetEntity="JobOrder", mappedBy="rider")
* @ORM\OrderBy({"priority" = "ASC"})
*/
protected $job_orders;
// rider's active job order since we now support multiple job orders per rider
/**
* @ORM\OneToOne(targetEntity="JobOrder")
* @ORM\JoinColumn(name="active_jo_id", referencedColumnName="id")
*/
protected $active_job_order;
// picture of rider
/**
* @ORM\Column(type="string", nullable=true)
@ -122,6 +130,8 @@ class Rider
$this->flag_active = true;
$this->username = null;
$this->password = '';
$this->active_job_order = null;
}
public function getID()
@ -300,8 +310,30 @@ class Rider
return $this->password;
}
public function setActiveJobOrder(JobOrder $jo = null)
{
$this->active_job_order = $jo;
return $this;
}
public function getActiveJobOrder()
{
// check if we have set a custom active
if ($this->active_job_order != null)
{
switch ($this->active_job_order->getStatus())
{
// if jo is open, return it
case JOStatus::ASSIGNED:
case JOStatus::IN_TRANSIT:
case JOStatus::IN_PROGRESS:
return $this->active_job_order;
}
// if active jo is not open, get the next open one
}
// no custom active job order
$active_status = [
JOStatus::ASSIGNED,
JOStatus::IN_TRANSIT,
@ -315,6 +347,20 @@ class Rider
return $this->job_orders->matching($criteria)[0];
}
public function getOpenJobOrders()
{
$active_status = [
JOStatus::ASSIGNED,
JOStatus::IN_TRANSIT,
JOStatus::IN_PROGRESS,
];
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->in('status', $active_status));
return $this->job_orders->matching($criteria);
}
public function getSessions()
{
return $this->sessions;

View file

@ -0,0 +1,66 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(name="service_charge")
*/
class ServiceCharge
{
// unique id
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
// name
/**
* @ORM\Column(type="string", length=80)
* @Assert\NotBlank()
*/
protected $name;
// amount
/**
* @ORM\Column(type="decimal", precision=9, scale=2)
*/
protected $amount;
public function __construct()
{
$this->amount = 0;
}
public function getID()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->name;
}
public function setAmount($amount)
{
$this->amount = $amount;
return $this;
}
public function getAmount()
{
return $this->amount;
}
}

View file

@ -149,7 +149,7 @@ class Warranty
return $this->id;
}
public function setSerial($serial)
public function setSerial($serial = null)
{
$this->serial = $serial;
return $this;

View file

@ -35,6 +35,7 @@ class JobOrderActiveCacheListener
$this->processActiveJO($jo);
break;
// inactive
// NOTE: should never really get here since it's creation
case JOStatus::CANCELLED:
$this->processInactiveJO($jo, 'cancel');
break;
@ -84,22 +85,55 @@ class JobOrderActiveCacheListener
$coords = $jo->getCoordinates();
// TODO: do we put the key in config?
// send jo location
$this->mqtt->publish(
'jo/' . $jo->getID() . '/location',
$coords->getLatitude() . ':' . $coords->getLongitude()
);
// TODO: do we still need to send jo status?
// send rider status
$rider = $jo->getRider();
if ($rider != null)
{
$this->mqtt->publish(
'rider/' . $rider->getID() . '/status',
'jo'
);
}
}
protected function processInactiveJO($jo, $status = 'cancel')
{
error_log('got inactive jo, sending mqtt message for ' . $jo->getID());
// remove from redis cache
$this->jo_cache->removeActiveJobOrder($jo);
// TODO: publich to mqtt
// publish to mqtt
// send jo status
$this->mqtt->publish(
'jo/' . $jo->getID() . '/status',
$status
);
// send rider status
$rider = $jo->getRider();
if ($rider != null)
{
// check if rider has any queued jobs
$open_jos = $rider->getOpenJobOrders();
if (count($open_jos) > 0)
$rider_status = 'jo';
else
$rider_status = 'available';
// send status
$this->mqtt->publish(
'rider/' . $rider->getID() . '/status',
$rider_status
);
}
}
}

View file

@ -5,6 +5,7 @@ namespace App\Ramcar;
use App\Entity\Battery;
use App\Entity\Promo;
use App\Entity\CustomerVehicle;
use App\Entity\ServiceCharge;
class InvoiceCriteria
{
@ -12,6 +13,8 @@ class InvoiceCriteria
protected $promos;
protected $cv;
protected $flag_coolant;
protected $discount;
protected $service_charges;
// entries are battery and trade-in combos
protected $entries;
@ -23,6 +26,8 @@ class InvoiceCriteria
$this->entries = [];
$this->cv = null;
$this->flag_coolant = false;
$this->discount = 0;
$this->service_charges = [];
}
public function setServiceType($stype)
@ -125,4 +130,27 @@ class InvoiceCriteria
{
return $this->flag_coolant;
}
public function setDiscount($discount)
{
$this->discount = $discount;
return $this;
}
public function getDiscount()
{
return $this->discount;
}
public function addServiceCharge(ServiceCharge $service_charge)
{
$this->service_charges[] = $service_charge;
return $this;
}
public function getServiceCharges()
{
return $this->service_charges;
}
}

View file

@ -9,12 +9,15 @@ class TransactionOrigin extends NameValue
const FACEBOOK = 'facebook';
const VIP = 'vip';
const MOBILE_APP = 'mobile_app';
const WALK_IN = 'walk_in';
// TODO: for now, resq also gets the walk-in option
const COLLECTION = [
'call' => 'Hotline',
'online' => 'Online',
'facebook' => 'Facebook',
'vip' => 'VIP',
'mobile_app' => 'Mobile App',
'walk_in' => 'Walk-in',
];
}

View file

@ -320,7 +320,9 @@ class CMBCustomerHandler implements CustomerHandlerInterface
$nerror_array = [];
$verror_array = [];
// TODO: validate mobile numbers
if (!($this->validateMobileNumber($req->request->get('phone_mobile'))))
$error_array['phone_mobile'] = 'Invalid mobile phone number.';
// TODO: validate vehicles
// custom validation for vehicles
@ -685,6 +687,18 @@ class CMBCustomerHandler implements CustomerHandlerInterface
}
}
public function validateMobileNumber($mobile_number)
{
if (empty($mobile_number))
return true;
if (strlen($mobile_number) != 9)
return false;
if(preg_match('/^\d+$/',$mobile_number))
return true;
return false;
}
// 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'])) {

View file

@ -17,8 +17,8 @@ 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\Entity\ServiceCharge;
use App\Service\InvoiceGeneratorInterface;
@ -106,9 +106,12 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
// break;
}
// TODO: check if any promo is applied
// apply discounts
$promos = $criteria->getPromos();
// process service charges if any
$service_charges = $criteria->getServiceCharges();
if (count($service_charges) > 0)
{
$this->processServiceCharges($total, $criteria, $invoice);
}
// get current user
$user = $this->security->getUser();
@ -131,7 +134,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
}
// generate invoice criteria
public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array)
public function generateInvoiceCriteria($jo, $discount, $invoice_items, &$error_array)
{
$em = $this->em;
@ -140,7 +143,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
$criteria->setServiceType($jo->getServiceType())
->setCustomerVehicle($jo->getCustomerVehicle());
$ierror = $this->invoicePromo($criteria, $promo_id);
$ierror = $this->validateDiscount($criteria, $discount);
if (!$ierror && !empty($invoice_items))
{
@ -158,11 +161,19 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
$ierror = $this->invoiceBatteries($criteria, $invoice_items);
}
// get the meta for service charges
$service_charges = $jo->getMeta('service_charges');
if (!empty($service_charges))
{
$service_charges = $jo->getMeta('service_charges');
$this->invoiceServiceCharges($criteria, $service_charges);
}
if ($ierror)
{
$error_array['invoice'] = $ierror;
}
else
{
// generate the invoice
@ -193,6 +204,25 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
}
}
// prepare draft for invoice
public function generateDraftInvoice($criteria, $discount, $service_charges, $items)
{
$ierror = $this->validateDiscount($criteria, $discount);
if (!$ierror)
{
// process service charges
$ierror = $this->invoiceServiceCharges($criteria, $service_charges);
if (!$ierror)
{
$ierror = $this->invoiceBatteries($criteria, $items);
}
}
return $ierror;
}
protected function getTaxAmount($price)
{
$vat_ex_price = $this->getTaxExclusivePrice($price);
@ -224,27 +254,26 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
return 0;
}
public function invoicePromo(InvoiceCriteria $criteria, $promo_id)
public function validateDiscount(InvoiceCriteria $criteria, $discount)
{
// 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))
// check if discount is blank or 0
if ((empty($discount)) || ($discount == 0))
{
return false;
}
// check if this is a valid promo
$promo = $this->em->getRepository(Promo::class)->find($promo_id);
// check if discount is greater than 50 or negative number
if (($discount > 50) || ($discount < 0))
return 'Invalid discount specified';
if (empty($promo))
return 'Invalid promo specified.';
$criteria->addPromo($promo);
$criteria->setDiscount($discount);
return false;
}
@ -292,6 +321,28 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
return null;
}
public function invoiceServiceCharges(InvoiceCriteria $criteria, $service_charges)
{
if (!empty($service_charges))
{
foreach ($service_charges as $service_charge)
{
// check if valid service charge
$sc = $this->em->getRepository(ServiceCharge::class)->find($service_charge['id']);
if (empty($sc))
{
$error = 'Invalid service charge specified.';
return $error;
}
$criteria->addServiceCharge($sc);
}
}
return null;
}
protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice)
{
@ -389,33 +440,14 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
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;
}
$discount = $criteria->getDiscount();
// if discount is higher than 0, display in invoice
if ($discount > 0)
{
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle('Promo discount')
->setTitle('Discount')
->setQuantity(1)
->setPrice(-1 * $discount);
$invoice->addItem($item);
@ -425,7 +457,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
$total['total_price'] -= $discount;
// process
$invoice->setPromo($promo);
$invoice->setDiscount($discount);
}
protected function processJumpstart(&$total, $invoice)
@ -629,4 +661,24 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface
$total['vat'] = $vat;
}
protected function processServiceCharges(&$total, InvoiceCriteria $criteria, Invoice $invoice)
{
$service_charges = $criteria->getServiceCharges();
foreach ($service_charges as $service_charge)
{
$amount = $service_charge->getAmount();
$title = 'Service Charge - ' . $service_charge->getName();
$total['total_price'] += $amount;
// add item
$item = new InvoiceItem();
$item->setInvoice($invoice)
->setTitle($title)
->setQuantity(1)
->setPrice($amount);
$invoice->addItem($item);
}
}
}

View file

@ -18,6 +18,7 @@ use App\Entity\Invoice;
use App\Entity\InvoiceItem;
use App\Entity\User;
use App\Entity\Battery;
use App\Entity\Promo;
use App\Service\InvoiceGeneratorInterface;
@ -193,6 +194,20 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface
}
}
// prepare draft for invoice
public function generateDraftInvoice($criteria, $promo_id, $service_charges, $items)
{
$ierror = $this->invoicePromo($criteria, $promo_id);
if (!$ierror)
{
$ierror = $this->invoiceBatteries($criteria, $items);
}
return $ierror;
}
protected function getTaxAmount($price)
{
$vat_ex_price = $this->getTaxExclusivePrice($price);
@ -226,7 +241,7 @@ class ResqInvoiceGenerator implements InvoiceGeneratorInterface
return 0;
}
public function invoicePromo(InvoiceCriteria $criteria, $promo_id)
protected function invoicePromo(InvoiceCriteria $criteria, $promo_id)
{
// return error if there's a problem, false otherwise
// check service type

View file

@ -15,4 +15,7 @@ interface InvoiceGeneratorInterface
// generate invoice criteria
public function generateInvoiceCriteria(JobOrder $jo, int $promo_id, array $invoice_items, array &$error_array);
// prepare draft for invoice
public function generateDraftInvoice(InvoiceCriteria $criteria, int $promo_id, array $service_charges, array $items);
}

View file

@ -26,6 +26,7 @@ use App\Entity\Rider;
use App\Entity\JORejection;
use App\Entity\Warranty;
use App\Entity\Customer;
use App\Entity\ServiceCharge;
use App\Ramcar\InvoiceCriteria;
use App\Ramcar\CMBServiceType;
@ -42,6 +43,7 @@ use App\Ramcar\JORejectionReason;
use App\Service\InvoiceGeneratorInterface;
use App\Service\JobOrderHandlerInterface;
use App\Service\RiderAssignmentHandlerInterface;
use App\Service\CustomerHandlerInterface;
use App\Service\WarrantyHandler;
use App\Service\MQTTClient;
use App\Service\APNSClient;
@ -66,13 +68,15 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
protected $rah;
protected $country_code;
protected $wh;
protected $cust_handler;
protected $template_hash;
public function __construct(Security $security, EntityManagerInterface $em,
InvoiceGeneratorInterface $ic, ValidatorInterface $validator,
TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah,
string $country_code, WarrantyHandler $wh)
string $country_code, WarrantyHandler $wh,
CustomerHandlerInterface $cust_handler)
{
$this->em = $em;
$this->ic = $ic;
@ -82,6 +86,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$this->rah = $rah;
$this->country_code = $country_code;
$this->wh = $wh;
$this->cust_handler = $cust_handler;
$this->loadTemplates();
}
@ -160,6 +165,25 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// process rows
$rows = [];
foreach ($obj_rows as $orow) {
// get car model
$cv = $orow->getCustomerVehicle();
$cv_manufacturer = $cv->getVehicle()->getManufacturer()->getName();
$cv_make = $cv->getVehicle()->getMake();
$year = $cv->getModelYear();
$car_model = $cv_manufacturer . ' ' . $cv_make . ' ' . $year;
// get rider information
$rider_name = '';
$rider_plate_number = '';
$rider = $orow->getRider();
if (!empty($rider))
{
$rider_name = $rider->getFullName();
$rider_plate_number = $rider->getPlateNumber();
}
// add row data
$row['id'] = $orow->getID();
$row['customer_name'] = $orow->getCustomer()->getFirstName() . ' ' . $orow->getCustomer()->getLastName();
@ -171,6 +195,9 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$row['flag_advance'] = $orow->isAdvanceOrder();
$row['plate_number'] = $orow->getCustomerVehicle()->getPlateNumber();
$row['is_mobile'] = $orow->getSource() == TransactionOrigin::MOBILE_APP;
$row['car_model'] = $car_model;
$row['rider_name'] = $rider_name;
$row['rider_plate_number'] = $rider_plate_number;
$processor = $orow->getProcessedBy();
if ($processor == null)
@ -347,13 +374,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// call service to generate job order and invoice
$invoice_items = $req->request->get('invoice_items', []);
$promo_id = $req->request->get('invoice_promo');
$discount = $req->request->get('invoice_discount');
$invoice_change = $req->request->get('invoice_change', 0);
// check if invoice changed
if ($invoice_change)
{
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $error_array);
$this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array);
}
// validate
@ -409,15 +436,21 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
}
// check if new customer
if ($req->request->get('new_customer'))
if ($req->request->get('new_customer', false))
{
if (empty($req->request->get('customer_customer_notes')))
{
$error_array['customer_customer_notes'] = 'Customer notes cannot be null.';
}
$new_cust = new Customer();
$new_cv = new CustomerVehicle();
// validate mobile phone
$valid_mobile = $this->cust_handler->validateMobileNumber($req->request->get('customer_phone_mobile'));
if (!($valid_mobile))
$error_array['customer_phone_mobile'] = 'Invalid mobile phone number.';
// check if plate number is in request
if (empty($req->request->get('cv_plate')))
$error_array['cv_plate'] = 'Plate number is required.';
// find the vehicle using vid
$new_vehicle = $em->getRepository(Vehicle::class)->find($req->request->get('vid'));
@ -426,8 +459,10 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$error_array['cv_mfg'] = 'Invalid manufacturer specified.';
$error_array['cv_make'] = 'Invalid make specified.';
}
else
if (empty($error_array))
{
$new_cust = new Customer();
$new_cv = new CustomerVehicle();
$new_cust->setLastName($req->request->get('customer_last_name'))
->setFirstName($req->request->get('customer_first_name'))
@ -514,6 +549,29 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
}
}
// set priority based on rider's existing open job orders
$rider_jos = $rider->getOpenJobOrders();
// get maximum priority then add 1
// NOTE: this can be a bit buggy due to concurrency issues
// ideally have to lock jo table, but that isn't feasible right now
$priority = 0;
foreach ($rider_jos as $rider_jo)
{
if ($priority < $rider_jo->getPriority())
$priority = $rider_jo->getPriority() + 1;
}
// get discount and set to meta
$discount = $req->request->get('invoice_discount', []);
// check if discount is greater than 50 or negative number
if (($discount > 50) || ($discount < 0))
$error_array['invoice_discount'] = 'Invalid discount specified';
// get list of service charges
$service_charges = $req->request->get('service_charges', []);
if (empty($error_array))
{
// get current user
@ -541,9 +599,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
->setModeOfPayment($req->request->get('mode_of_payment'))
->setLandmark($req->request->get('landmark'))
->setHub($hub)
->setRider($rider);
->setRider($rider)
->setPriority($priority);
// check if user is null, meaning call to create came from API
$jo->addMeta('discount', $discount);
$jo->addMeta('service_charges', $service_charges);
// check if user is null, meaning call to create came from API
if ($user != null)
{
$jo->setCreatedBy($user);
@ -563,13 +625,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// call service to generate job order and invoice
$invoice_items = $req->request->get('invoice_items', []);
$promo_id = $req->request->get('invoice_promo');
$discount = $req->request->get('invoice_discount');
$invoice_change = $req->request->get('invoice_change', 0);
// check if invoice changed
if ($invoice_change)
{
$this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $error_array);
$this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array);
}
// validate
@ -918,7 +980,11 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// create the warranty if new battery only
if ($this->checkIfNewBattery($obj))
{
$serial = $req->request->get('warranty_code') ;
if (empty($req->request->get('warranty_code')))
$serial = null;
else
$serial = $req->request->get('warranty_code');
$warranty_class = $obj->getWarrantyClass();
$first_name = $obj->getCustomer()->getFirstName();
$last_name = $obj->getCustomer()->getLastName();
@ -1361,12 +1427,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
{
$params['obj'] = new JobOrder();
$params['mode'] = 'onestep';
$params['jo_service_charges'] = [];
$this->fillDropdownParameters($params);
$this->fillFormTags($params);
// get template to display
$params['template'] = $this->getTwigTemplate('jo_onestep');
$params['template'] = $this->getTwigTemplate('jo_onestep_form');
// return params
return $params;
@ -1382,6 +1449,22 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$params['cvid'] = $obj->getCustomerVehicle()->getID();
$params['vid'] = $obj->getCustomerVehicle()->getVehicle()->getID();
// get service charges
$sc_array = [];
$jo_service_charges = $obj->getMeta('service_charges');
if (!(empty($jo_service_charges)))
{
foreach ($jo_service_charges as $jo_sc_id)
{
// find service charge
$sc_obj = $em->getRepository(ServiceCharge::class)->find($jo_sc_id);
$sc_array[] = $sc_obj;
}
}
$params['jo_service_charges'] = $sc_array;
$this->fillDropdownParameters($params);
$this->fillFormTags($params);
@ -1490,7 +1573,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
{
$em = $this->em;
$params['mode'] = 'update-all';
$params['mode'] = 'view-all';
// get row data
$obj = $em->getRepository(JobOrder::class)->find($id);
@ -1502,8 +1585,12 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$this->fillDropdownParameters($params);
$this->fillFormTags($params);
// get template to display
$params['template'] = $this->getTwigTemplate('jo_all_form');
// get template to display
// check transaction origin if walkin
if ($obj->getSource() == TransactionOrigin::WALK_IN)
$params['template'] = $this->getTwigTemplate('jo_walkin_form');
else
$params['template'] = $this->getTwigTemplate('jo_onestep_form');
$params['obj'] = $obj;
$params['status_cancelled'] = JOStatus::CANCELLED;
@ -2022,13 +2109,6 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$pdf->Cell($label_width, $line_height, 'Plate Number:');
$pdf->MultiCell($val_width, $line_height, $cv ? $cv->getPlateNumber() : '', 0, 'L');
// get Y after left cell
$y1 = $pdf->GetY();
$pdf->SetXY($col2_x, $y);
$pdf->Cell($label_width, $line_height, 'Vehicle Color:');
$pdf->MultiCell(0, $line_height, $cv ? $cv->getColor() : '', 0, 'L');
// get Y after right cell
$y2 = $pdf->GetY();
@ -2345,6 +2425,311 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
return false;
}
public function initializeWalkinForm()
{
$params['obj'] = new JobOrder();
$params['mode'] = 'walk-in';
$this->fillDropdownParameters($params);
$this->fillFormTags($params);
// get template to display
$params['template'] = $this->getTwigTemplate('jo_walkin_form');
// return params
return $params;
}
public function processWalkinJobOrder(Request $req, $id)
{
// initialize error list
$error_array = [];
$em = $this->em;
$jo = $em->getRepository(JobOrder::class)->find($id);
if (empty($jo))
{
// new job order
$jo = new JobOrder();
}
// check if new customer
if ($req->request->get('new_customer', false))
{
if (empty($req->request->get('customer_customer_notes')))
{
$error_array['customer_customer_notes'] = 'Customer notes cannot be null.';
}
// validate mobile phone
$valid_mobile = $this->cust_handler->validateMobileNumber($req->request->get('customer_phone_mobile'));
if (!($valid_mobile))
$error_array['customer_phone_mobile'] = 'Invalid mobile phone number.';
// check if plate number is in request
if (empty($req->request->get('cv_plate')))
$error_array['cv_plate'] = 'Plate number is required.';
// find the vehicle using vid
$new_vehicle = $em->getRepository(Vehicle::class)->find($req->request->get('vid'));
if (empty($new_vehicle))
{
$error_array['cv_mfg'] = 'Invalid manufacturer specified.';
$error_array['cv_make'] = 'Invalid make specified.';
}
if (empty($error_array))
{
$new_cust = new Customer();
$new_cv = new CustomerVehicle();
$new_cust->setLastName($req->request->get('customer_last_name'))
->setFirstName($req->request->get('customer_first_name'))
->setPhoneMobile($req->request->get('customer_phone_mobile'))
->setPhoneLandline($req->request->get('customer_phone_landline'))
->setPhoneOffice($req->request->get('customer_phone_office'))
->setPhoneFax($req->request->get('customer_phone_fax'))
->setCustomerNotes($req->request->get('customer_customer_notes'));
$new_cv->setCustomer($new_cust)
->setVehicle($new_vehicle)
->setPlateNumber($req->request->get('cv_plate'))
->setModelYear($req->request->get('cv_year'))
->setColor('')
->setStatusCondition('')
->setFuelType('')
->setActive()
->setWarrantyCode($req->request->get('warranty_code'));
if (($req->request->get('service_type')) == CMBServiceType::BATTERY_REPLACEMENT_NEW)
{
$new_cv->setHasMotoliteBattery(true);
}
else
{
$new_cv->setHasMotoliteBattery(false);
}
// link JO to new customer
$jo->setCustomer($new_cust);
$jo->setCustomerVehicle($new_cv);
$em->persist($new_cust);
$em->persist($new_cv);
}
}
else
{
// check if customer vehicle is set
if (empty($req->request->get('customer_vehicle'))) {
$error_array['customer_vehicle'] = 'No vehicle selected.';
} else
{
// get customer vehicle
$cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($req->request->get('customer_vehicle'));
if (empty($cust_vehicle)) {
$error_array['customer_vehicle'] = 'Invalid vehicle specified.';
}
else
{
$jo->setCustomerVehicle($cust_vehicle);
$jo->setCustomer($cust_vehicle->getCustomer());
// save serial into cv
$cust_vehicle->setWarrantyCode($req->request->get('warranty_code'));
$em->persist($cust_vehicle);
}
}
}
// check if hub is selected
if (empty($req->request->get('hub_id')))
$error_array['hub'] = 'No hub selected.';
else
{
// get hub
$hub = $em->getRepository(Hub::class)->find($req->request->get('hub_id'));
if (empty($hub))
$error_array['hub'] = 'Invalid hub specified.';
// get hub coordinates
$hub_coordinates = $hub->getCoordinates();
}
// get discount and set to meta
$discount = $req->request->get('invoice_discount');
// check if discount is greater than 50 or negative number
if (($discount > 50) || ($discount < 0))
$error_array['invoice_discount'] = 'Invalid discount specified';
if (empty($error_array))
{
// get current user
$user = $this->security->getUser();
$stype = $req->request->get('service_type');
// set and save values
$jo->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time')))
->setAdvanceOrder($req->request->get('flag_advance') ?? false)
->setServiceType($stype)
->setWarrantyClass($req->request->get('warranty_class'))
->setSource($req->request->get('source'))
->setStatus(JOStatus::FULFILLED)
->setTier1Notes($req->request->get('tier1_notes'))
->setTier2Notes($req->request->get('tier2_notes'))
->setORName($req->request->get('or_name'))
->setPromoDetail($req->request->get('promo_detail'))
->setModeOfPayment($req->request->get('mode_of_payment'))
->setLandmark($req->request->get('landmark'))
->setDeliveryAddress('Walk-in')
->setLandmark('Walk-in')
->setCoordinates($hub_coordinates)
->setHub($hub);
$jo->addMeta('discount', $discount);
// check if user is null, meaning call to create came from API
if ($user != null)
{
$jo->setCreatedBy($user);
}
// check if reference JO is set and validate
if (!empty($req->request->get('ref_jo'))) {
// get reference JO
$ref_jo = $em->getRepository(JobOrder::class)->find($req->request->get('ref_jo'));
if (empty($ref_jo)) {
$error_array['ref_jo'] = 'Invalid reference job order specified.';
} else {
$jo->setReferenceJO($ref_jo);
}
}
// call service to generate job order and invoice
$invoice_items = $req->request->get('invoice_items', []);
$discount = $req->request->get('invoice_discount');
$invoice_change = $req->request->get('invoice_change', 0);
// check if invoice changed
if ($invoice_change)
{
$this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array);
}
// validate
$errors = $this->validator->validate($jo);
// add errors to list
foreach ($errors as $error) {
$error_array[$error->getPropertyPath()] = $error->getMessage();
}
// check if errors are found
if (empty($error_array))
{
// validated, no error. save the job order
$em->persist($jo);
// the event
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::CREATE)
->setJobOrder($jo);
if ($user != null)
{
$event->setUser($user);
}
$em->persist($event);
// save to customer vehicle battery record
$this->updateVehicleBattery($jo);
// save serial to customer vehicle
$cust_vehicle = $jo->getCustomerVehicle();
$cust_vehicle->setWarrantyCode($req->request->get('warranty_code'));
$em->persist($cust_vehicle);
// create the warranty if new battery only
if ($this->checkIfNewBattery($jo))
{
if (empty($req->request->get('warranty_code')))
$serial = null;
else
$serial = $req->request->get('warranty_code');
$warranty_class = $jo->getWarrantyClass();
$first_name = $jo->getCustomer()->getFirstName();
$last_name = $jo->getCustomer()->getLastName();
$mobile_number = $jo->getCustomer()->getPhoneMobile();
// check if date fulfilled is null
if ($jo->getDateFulfill() == null)
$date_purchase = $jo->getDateCreate();
else
$date_purchase = $jo->getDateFulfill();
// validate plate number
// $plate_number = $this->wh->cleanPlateNumber($jo->getCustomerVehicle()->getPlateNumber());
$plate_number = Warranty::cleanPlateNumber($jo->getCustomerVehicle()->getPlateNumber());
if ($plate_number != false)
{
$batt_list = array();
$invoice = $jo->getInvoice();
if (!empty($invoice))
{
// get battery
$invoice_items = $invoice->getItems();
foreach ($invoice_items as $item)
{
$battery = $item->getBattery();
if ($battery != null)
{
$batt_list[] = $item->getBattery();
}
}
}
$this->wh->createWarranty($serial, $plate_number, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase, $warranty_class);
}
}
$em->flush();
}
}
return $error_array;
}
public function initializeWalkinEditForm($id)
{
$em = $this->em;
$obj = $em->getRepository(JobOrder::class)->find($id);
$params['obj'] = $obj;
$params['mode'] = 'walk-in-edit';
$params['cvid'] = $obj->getCustomerVehicle()->getID();
$params['vid'] = $obj->getCustomerVehicle()->getVehicle()->getID();
$this->fillDropdownParameters($params);
$this->fillFormTags($params);
// get template to display
$params['template'] = $this->getTwigTemplate('jo_walkin_edit_form');
// return params
return $params;
}
protected function fillDropdownParameters(&$params)
{
$em = $this->em;
@ -2352,6 +2737,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// db loaded
$params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll();
$params['promos'] = $em->getRepository(Promo::class)->findAll();
$params['service_charges'] = $em->getRepository(ServiceCharge::class)->findAll();
// list of hubs
$hubs = $em->getRepository(Hub::class)->findBy([], ['name' => 'ASC']);
@ -2422,6 +2808,18 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
$params['ftags']['invoice_edit'] = true;
$params['ftags']['preset_vehicle'] = true;
break;
case 'walk-in':
$params['ftags']['vehicle_dropdown'] = true;
$params['ftags']['set_map_coordinate'] = false;
$params['ftags']['invoice_edit'] = true;
$params['ftags']['ticket_table'] = false;
$params['ftags']['cancel_button'] = false;
break;
case 'walk-in-edit':
$params['ftags']['invoice_edit'] = true;
$params['ftags']['preset_vehicle'] = true;
break;
}
}
@ -2434,22 +2832,16 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
// $this->template_hash = [
// 'blah' => 'blah',
// ];
$this->template_hash['jo_incoming_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_open_edit_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_incoming_vehicle_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_processing_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_assigning_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_fulfillment_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_open_hub_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_open_rider_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_all_form'] = 'job-order/cmb.form.html.twig';
$this->template_hash['jo_list_processing'] = 'job-order/list.processing.html.twig';
$this->template_hash['jo_list_assigning'] = 'job-order/list.assigning.html.twig';
$this->template_hash['jo_list_fulfillment'] = 'job-order/list.fulfillment.html.twig';
$this->template_hash['jo_list_open'] = 'job-order/list.open.html.twig';
$this->template_hash['jo_list_all'] = 'job-order/list.all.html.twig';
$this->template_hash['jo_onestep'] = 'job-order/cmb.form.onestep.html.twig';
$this->template_hash['jo_fulfillment_form'] = 'job-order/cmb.form.onestep.html.twig';
$this->template_hash['jo_all_form'] = 'job-order/cmb.form.onestep.html.twig';
$this->template_hash['jo_list_fulfillment'] = 'job-order/cmb.list.fulfillment.html.twig';
$this->template_hash['jo_list_open'] = 'job-order/cmb.list.open.html.twig';
$this->template_hash['jo_list_all'] = 'job-order/cmb.list.all.html.twig';
$this->template_hash['jo_onestep_form'] = 'job-order/cmb.form.onestep.html.twig';
$this->template_hash['jo_onestep_edit_form'] = 'job-order/cmb.form.onestep.html.twig';
$this->template_hash['jo_walkin_form'] = 'job-order/cmb.form.walkin.html.twig';
$this->template_hash['jo_walkin_edit_form'] = 'job-order/cmb.form.walkin.html.twig';
}
protected function checkTier($tier)
@ -2641,4 +3033,17 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface
->setParameter('status', $status);
}
}
public function getEditRoute($jo_id, $tier = null)
{
$jo = $this->em->getRepository(JobOrder::class)->find($jo_id);
if (empty($jo))
throw new NotFoundHttpException('The item does not exist');
// check transaction origin
if ($jo->getSource() == TransactionOrigin::WALK_IN)
return 'jo_walkin_edit_form';
else
return 'jo_onestep_edit_form';
}
}

View file

@ -314,6 +314,9 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
}
}
// TODO: check status before saving since JO might already
// have a status that needs to be retained
if (empty($error_array)) {
// get current user
$user = $this->security->getUser();
@ -2587,4 +2590,12 @@ class ResqJobOrderHandler implements JobOrderHandlerInterface
->setParameter('status', $status);
}
}
public function getEditRoute($jo_id, $tier)
{
if (empty($tier))
return 'jo_open_edit_form';
return $tier;
}
}

View file

@ -98,4 +98,7 @@ interface JobOrderHandlerInterface
// check if service type is new battery
public function checkIfNewBattery(JobOrder $jo);
// return the edit route, based on tier and form
public function getEditRoute(int $jo_id, $tier);
}

View file

@ -73,8 +73,11 @@ class WarrantyHandler
}
// set and save values
$warranty->setSerial($serial)
->setPlateNumber($plate_number)
if (trim($serial) == '')
$warranty->setSerial(null);
else
$warranty->setSerial($serial);
$warranty->setPlateNumber($plate_number)
->setFirstName($first_name)
->setLastName($last_name)
->setMobileNumber($mobile_number)

View file

@ -51,6 +51,7 @@ function initEventHandler(dashmap) {
'track_rider': true,
'channels': {
'rider_location': 'rider/+/location',
'rider_status': 'rider/+/status',
'jo_location': 'jo/+/location',
'jo_status': 'jo/+/status'
},

View file

@ -184,7 +184,9 @@
</div>
<div class="form-group m-form__group row">
<div class="col-lg-3">
<label data-field="cv_plate">Plate #</label>
<label data-field="cv_plate">Plate #
<span style="color:red"> *</span>
</label>
<input type="text" name="cv_plate" id="cv-plate" class="form-control m-input cv_field" value="{{ obj.getCustomerVehicle.getPlateNumber|default('') }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="cv_plate"></div>
</div>
@ -206,17 +208,17 @@
<div class="form-group m-form__group row">
<div class="col-lg-3">
<label data-field="current_battery">Current Battery</label>
<input type="text" name="current_battery" id="current-battery" class="form-control m-input" value="{{ obj.getCustomerVehicle and obj.getCustomerVehicle.getCurrentBattery ? obj.getCustomerVehicle.getCurrentBattery.getManufacturer.getName ~ ' ' ~ obj.getCustomerVehicle.getCurrentBattery.getModel.getName ~ ' ' ~ obj.getCustomerVehicle.getCurrentBattery.getSize.getName ~ ' (' ~ obj.getCustomerVehicle.getCurrentBattery.getProductCode ~ ')' }}" data-vehicle-field="1" disabled>
<input type="text" name="current_battery" id="current-battery" class="form-control m-input battery_field" value="{{ obj.getCustomerVehicle and obj.getCustomerVehicle.getCurrentBattery ? obj.getCustomerVehicle.getCurrentBattery.getManufacturer.getName ~ ' ' ~ obj.getCustomerVehicle.getCurrentBattery.getModel.getName ~ ' ' ~ obj.getCustomerVehicle.getCurrentBattery.getSize.getName ~ ' (' ~ obj.getCustomerVehicle.getCurrentBattery.getProductCode ~ ')' }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="current_battery"></div>
</div>
<div class="col-lg-3">
<label data-field="warranty_code">Serial Number</label>
<input type="text" name="warranty_code" id="warranty-code" class="form-control m-input" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getWarrantyCode }}">
<input type="text" name="warranty_code" id="warranty-code" class="form-control m-input battery_field" value="{{ obj.getCustomerVehicle ? obj.getCustomerVehicle.getWarrantyCode }}">
<div class="form-control-feedback hide" data-field="warranty_code"></div>
</div>
<div class="col-lg-3">
<label data-field="warranty_expiration">Warranty Expiration Date</label>
<input type="text" name="warranty_expiration" id="warranty-expiration" class="form-control m-input" value="{{ obj.getCustomerVehicle.getCurrentBattery|default(false) ? obj.getCustomerVehicle.getWarrantyExpiration|date("d M Y") : '' }}" data-vehicle-field="1" disabled>
<input type="text" name="warranty_expiration" id="warranty-expiration" class="form-control m-input battery_field" value="{{ obj.getCustomerVehicle.getCurrentBattery|default(false) ? obj.getCustomerVehicle.getWarrantyExpiration|date("d M Y") : '' }}" data-vehicle-field="1" disabled>
<div class="form-control-feedback hide" data-field="warranty_expiration"></div>
</div>
</div>
@ -319,7 +321,7 @@
</div>
<br>
<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>
</div>
</div>
@ -389,14 +391,20 @@
<div class="m-form__section">
<div class="m-form__heading">
<h3 class="m-form__heading-title">
Nearest Hubs
{% if mode in ['view-all', 'update-fulfillment'] %}
Assigned Hub
{% else %}
Nearest Hubs
{% endif %}
</h3>
</div>
<div class="form-group m-form__group row">
<div class="col-lg-12">
<label name="hub" data-field="hub">Click on a row to select a hub
<span style="color:red"> *</span>
</label>
{% if mode != 'view-all' %}
<label name="hub" data-field="hub">Click on a row to select a hub
<span style="color:red"> *</span>
</label>
{% endif %}
<div class="form-control-feedback hide" data-field="hub"></div>
<input type="hidden" id="hub-field" name="hub_id" value="">
<div class="table-frame" data-name="hub">
@ -430,14 +438,20 @@
<div class="m-form__section">
<div class="m-form__heading">
<h3 class="m-form__heading-title">
Rider Assignment
{% if mode in ['view-all', 'update-fulfillment'] %}
Assigned Rider
{% else %}
Rider Assignment
{% endif %}
</h3>
</div>
<div class="form-group m-form__group row">
<div class="col-lg-12">
<label name="rider" data-field="rider">Click on a row to select a rider
<span style="color:red"> *</span>
</label>
{% if mode != 'view-all' %}
<label name="rider" data-field="rider">Click on a row to select a rider
<span style="color:red"> *</span>
</label>
{% endif %}
<div class="form-control-feedback hide" data-field="rider"></div>
<input type="hidden" id="rider-field" name="rider_id" value="">
<div class="table-frame" data-name="rider">
@ -452,7 +466,7 @@
</tr>
</thead>
<tbody id="riders">
{% if mode in ['onestep-edit'] %}
{% if mode in ['onestep-edit', 'view-all', 'update-fulfillment'] %}
{% set avail_riders = obj.getHub.getAvailableRiders|default([]) %}
<tr class="placeholder-row{{ obj.getHub and avail_riders|length > 0 ? ' hide' }}">
<td colspan="5">
@ -462,7 +476,11 @@
{% if obj.getHub %}
{% for rider in avail_riders %}
<tr data-id="{{ rider.getID }}"{{ obj.getRider and obj.getRider.getID == rider.getID ? ' class="m-table__row--primary"' }}">
{% if mode in ['view-all', 'update-fulfillment'] %}
<tr data-id="{{ rider.getID }}"{{ obj.getRider and obj.getRider.getID == rider.getID ? ' ' }}">
{% else %}
<tr data-id="{{ rider.getID }}"{{ obj.getRider and obj.getRider.getID == rider.getID ? ' class="m-table__row--primary"' }}">
{% endif %}
<td>{{ rider.getFirstName }}</td>
<td>{{ rider.getLastName }}</td>
<td>{{ rider.getContactNumber }}</td>
@ -477,6 +495,42 @@
</div>
</div>
<div class="m-form__seperator m-form__seperator--dashed"></div>
<div class="m-form__section" id="sc-section">
<div class="m-form__heading">
<h3 class="m-form__heading-title">
Service Charges
</h3>
</div>
<div class="row m-form__group">
{% if mode != 'view-all' %}
<div class="col-lg-12">
<button class="btn btn-primary" id="btn-sc-add">Add Service Charge</button>
</div>
{% endif %}
</div>
<!-- loop through existing service charges for job order -->
{% for jo_sc in jo_service_charges %}
<div class="form-group m-form__group row">
<div class="col-lg-6">
<div class="col-lg-12 form-group-inner">
<select class="form-control m-input sc-select" name="service_charges">
{% for key, sc in service_charges %}
<option value="{{ sc.getID }}" data-amount="{{ sc.getAmount }}"{{ jo_sc.getID == sc.getID ? ' selected' }}>{{ sc.getName }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-lg-5">
<input class="form-control sc-amount" type="text" value="{{ jo_sc.getAmount }}" disabled>
</div>
<div class="col-lg-1">
<button class="btn btn-danger btn-sc-remove">X</button>
</div>
</div>
{% endfor %}
</div>
<div class="m-form__seperator m-form__seperator--dashed"></div>
<div class="m-form__section m-form__section--last">
<div class="m-form__heading">
@ -498,24 +552,15 @@
</div>
<div class="form-group m-form__group row">
<div class="col-lg-6">
<label>Discount Type</label>
<label>Discount</label>
{% if ftags.invoice_edit %}
<select class="form-control m-input" id="invoice-promo" name="invoice_promo">
<option value="">None</option>
{% for promo in promos %}
<option value="{{ promo.getID() }}">{{ promo.getName() ~ ' (' ~ promo.getDiscountRate * 100 ~ '% applied to ' ~ discount_apply[promo.getDiscountApply] ~ ')' }}</option>
{% endfor %}
</select>
<div class="form-control-feedback hide" data-field="invoice_promo"></div>
<input type="number" id="invoice-discount" name="invoice_discount" class="form-control m-input min = "0" max="50" value="{{ obj.getInvoice ? obj.getInvoice.getDiscount }}">
<div class="form-control-feedback hide" data-field="invoice_discount"></div>
{% else %}
<input type="text" id="invoice-promo" class="form-control m-input" value="{{ obj.getInvoice.getPromo.getName|default('None') }}" disabled>
<input type="number" id="invoice-discount" name="invoice_discount" class="form-control m-input min="0" max="50" value="{{ obj.getInvoice ? obj.getInvoice.getDiscount }}" disabled>
{% endif %}
</div>
<div class="col-lg-3">
<label>Promo Discount</label>
<input type="text" id="invoice-promo-discount" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getDiscount|number_format(2) : '0.00' }}" disabled>
</div>
<div class="col-lg-3">
<div class="col-lg-6">
<label>Trade In</label>
<input type="text" id="invoice-trade-in" class="form-control m-input text-right" value="{{ obj.getInvoice ? obj.getInvoice.getTradeIn|number_format(2) : '0.00' }}" disabled>
</div>
@ -609,9 +654,11 @@
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
{% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %}
<a href="{{ url('jo_cancel', {'id': obj.getID}) }}" class="btn btn-danger btn-cancel-job-order">Cancel Job Order</a>
{% if mode != 'view-all' %}
<button type="submit" class="btn btn-success">{{ mode == 'update-fulfillment' ? 'Fulfill' : 'Submit' }}</button>
{% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %}
<a href="{{ url('jo_cancel', {'id': obj.getID}) }}" class="btn btn-danger btn-cancel-job-order">Cancel Job Order</a>
{% endif %}
{% endif %}
<a href="{{ return_url }}" class="btn btn-secondary">Back</a>
</div>
@ -756,6 +803,32 @@ $(function() {
});
{% endif %}
{% if mode in ['view-all', 'update-fulfillment'] %}
var hub_table = '';
$.getJSON("{{ url('hub_nearest') }}?lat=" + lat + "&long=" + lng, function(data) {
var hubs = data['hubs'];
var hub_marker;
for (i in hubs) {
var hub = hubs[i];
if(selected_hub == hub['id']) {
hub_table += '<tr data-id=' + hub['id'] + ' ' + '>';
hub_marker = L.marker([hub['lat'], hub['long']], { icon: icon_hub });
hubLayerGroup.addLayer(hub_marker);
hub_table += '<td>' + hub['name'] + '</td>';
hub_table += '<td>' + hub['branch'] + '</td>';
hub_table += '<td>' + hub['cnum'] + '</td>';
hub_table += '<td>' + hub['distance'] + '</td>';
hub_table += '<td></td>';
hub_table += '</tr>';
}
}
$('#nearest_hubs').html(hub_table);
});
{% endif %}
// add marker to layer group
markerLayerGroup.addLayer(marker);
@ -819,76 +892,80 @@ $(function() {
});
$(function() {
$('#hubs-table').on('click', 'tr', function() {
var id = $(this).data('id');
{% if mode != 'view-all' %}
$('#hubs-table').on('click', 'tr', function() {
var id = $(this).data('id');
riderLayerGroup.clearLayers();
riderLayerGroup.clearLayers();
if (id != selected_hub) {
if (id != selected_hub) {
// highlight this row
$('#hubs-table').find('.m-table__row--primary').removeClass('m-table__row--primary');
// highlight this row
$('#hubs-table').find('.m-table__row--primary').removeClass('m-table__row--primary');
$(this).addClass('m-table__row--primary');
$(this).addClass('m-table__row--primary');
// set hub
selected_hub = id;
$('#hub-field').val(selected_hub);
// set hub
selected_hub = id;
$('#hub-field').val(selected_hub);
// clear rider field
$('#rider-field').val('');
selected_rider = '';
// clear rider field
$('#rider-field').val('');
selected_rider = '';
// get riders of hub
// get hub riders ajax
// TODO: add latitude and longitude of delivery location to ajax request
var rider_table = '';
$.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) {
var riders = data['riders'];
for (i in riders) {
var rider = riders[i];
var rider_lat = rider['location'][0];
var rider_lng = rider['location'][1];
var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available });
riderLayerGroup.addLayer(rider_marker);
// get riders of hub
// get hub riders ajax
// TODO: add latitude and longitude of delivery location to ajax request
var rider_table = '';
$.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) {
var riders = data['riders'];
for (i in riders) {
var rider = riders[i];
var rider_lat = rider['location'][0];
var rider_lng = rider['location'][1];
var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available });
riderLayerGroup.addLayer(rider_marker);
rider_table += '<tr data-id=' + rider['id'] + '>';
rider_table += '<td>' + rider['first_name'] + '</td>';
rider_table += '<td>' + rider['last_name'] + '</td>';
rider_table += '<td>' + rider['contact_num'] + '</td>';
rider_table += '<td>' + rider['plate_num'] + '</td>';
rider_table += '<td></td>';
rider_table += '</tr>';
}
rider_table += '<tr data-id=' + rider['id'] + '>';
rider_table += '<td>' + rider['first_name'] + '</td>';
rider_table += '<td>' + rider['last_name'] + '</td>';
rider_table += '<td>' + rider['contact_num'] + '</td>';
rider_table += '<td>' + rider['plate_num'] + '</td>';
rider_table += '<td></td>';
rider_table += '</tr>';
}
$('#riders').html(rider_table);
});
} else {
// unhighlight this row
$(this).removeClass('m-table__row--primary');
// remove id value
selected_hub = '';
}
});
$('#riders').html(rider_table);
});
} else {
// unhighlight this row
$(this).removeClass('m-table__row--primary');
// remove id value
selected_hub = '';
}
});
{% endif %}
});
$(function() {
$('#rider-table').on('click', 'tr', function() {
var id = $(this).data('id');
{% if mode != 'view-all' %}
$('#rider-table').on('click', 'tr', function() {
var id = $(this).data('id');
// highlight this row
$('#rider-table').find('.m-table__row--primary').removeClass('m-table__row--primary');
// highlight this row
$('#rider-table').find('.m-table__row--primary').removeClass('m-table__row--primary');
$(this).addClass('m-table__row--primary');
$(this).addClass('m-table__row--primary');
// set rider
selected_rider = id;
$('#rider-field').val(selected_rider);
});
// set rider
selected_rider = id;
$('#rider-field').val(selected_rider);
});
{% endif %}
});
{% if mode in ['onestep-edit'] %}
{% if mode in ['onestep-edit', 'view-all', 'update-fulfillment'] %}
var lat = {{ obj.getCoordinates.getLatitude }};
var lng = {{ obj.getCoordinates.getLongitude }};
@ -969,6 +1046,9 @@ $(function() {
// add invoice items to data
fields['invoice_items'] = invoiceItems;
// add service charges to data
fields['service_charges'] = sc_array;
{% if mode in ['update-processing', 'update-reassign-hub'] %}
// add selected hub to data
fields['hub'] = selectedHub;
@ -1192,6 +1272,7 @@ $(function() {
$('.cust_field').val('');
$('.cv_field').val('');
$('#cv-make').val('');
$('.battery_field').val('');
} else {
$('.cust_field').prop('disabled', true);
$('.cv_field').prop('disabled', true);
@ -1276,6 +1357,22 @@ $(function() {
});
var invoiceItems = [];
var sc_array = [];
// populate invoiceItems if editing so that we don't lose the battery
{% if mode in ['view-all', 'open-edit', 'onestep-edit', 'walk-in-edit', 'update-fulfillment'] %}
{% if (obj.getInvoice and obj.getInvoice.getItems|length > 0) %}
{% for item in obj.getInvoice.getItems %}
{% if item.getBattery() %}
invoiceItems.push({
battery: {{ item.getBattery().getID() }},
quantity: {{ item.getQuantity() }},
trade_in: {{ obj.getInvoice().getTradeIn }},
});
{% endif %}
{% endfor %}
{% endif %}
{% endif %}
// add to invoice
$("#btn-add-to-invoice").click(function() {
@ -1316,7 +1413,7 @@ $(function() {
});
// update invoice when promo is changed
$("#invoice-promo").change(function() {
$("#invoice-discount").change(function() {
generateInvoice();
});
@ -1325,72 +1422,84 @@ $(function() {
generateInvoice();
});
// reset the invoice table
$("#btn-reset-invoice").click(function() {
$("#invoice-promo").prop('selectedIndex', 0);
invoiceItems = [];
generateInvoice();
});
// reset the invoice table
$("#btn-reset-invoice").click(function() {
$("#invoice-discount").val(0);
$('.sc-select').closest('.row').remove();
invoiceItems = [];
generateInvoice();
});
// recompute
$("#btn-recompute-invoice").click(function() {
generateInvoice();
});
function generateInvoice() {
var promo = $("#invoice-promo").val();
var table = $("#invoice-table tbody");
function generateInvoice() {
var discount = $("#invoice-discount").val();
var table = $("#invoice-table tbody");
var stype = $("#service_type").val();
var cvid = $("#customer-vehicle").val();
// generate invoice values
$.ajax({
method: "POST",
url: "{{ url('jo_gen_invoice') }}",
data: {
sc_array = [];
// get the service charges
$('.sc-select').each(function() {
var id = $(this).children('option:selected').val();
sc_array.push({
id: id,
});
});
// generate invoice values
$.ajax({
method: "POST",
url: "{{ url('jo_gen_invoice') }}",
data: {
'stype': stype,
'items': invoiceItems,
'promo': promo,
'cvid': cvid
}
}).done(function(response) {
'items': invoiceItems,
'promo': discount,
'cvid': cvid,
'service_charges': sc_array,
}
}).done(function(response) {
// mark as invoice changed
$("#invoice-change").val(1);
var invoice = response.invoice;
var invoice = response.invoice;
// populate totals
$("#invoice-promo-discount").val(invoice.discount);
$("#invoice-price").val(invoice.price);
$("#invoice-trade-in").val(invoice.trade_in);
$("#invoice-vat").val(invoice.vat);
$("#invoice-total-amount").val(invoice.total_price);
// populate totals
$("#invoice-discount").val(invoice.discount);
$("#invoice-price").val(invoice.price);
$("#invoice-trade-in").val(invoice.trade_in);
$("#invoice-vat").val(invoice.vat);
$("#invoice-total-amount").val(invoice.total_price);
// populate rows
var html = '';
// populate rows
var html = '';
if (invoice.items.length > 0) {
$.each(invoice.items, function(key, item) {
html += '<tr data-key="' + key + '">' +
'<td>' + item.title + '</td>' +
'<td class="text-right">' + item.quantity + '</td>' +
'<td class="text-right">' + item.unit_price + '</td>' +
'<td class="text-right">' + item.amount + '</td>' +
/*
'<td><button type="button" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-invoice-delete" title="Delete"><i class="la la-trash"></i></button></td>' +
*/
'</tr>';
});
} else {
html = '<tr class="placeholder-row">' +
'<td colspan="4">' +
'No items to display.' +
'</td>' +
'</tr>';
}
table.html(html);
});
}
if (invoice.items.length > 0) {
$.each(invoice.items, function(key, item) {
html += '<tr data-key="' + key + '">' +
'<td>' + item.title + '</td>' +
'<td class="text-right">' + item.quantity + '</td>' +
'<td class="text-right">' + item.unit_price + '</td>' +
'<td class="text-right">' + item.amount + '</td>' +
/*
'<td><button type="button" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-invoice-delete" title="Delete"><i class="la la-trash"></i></button></td>' +
*/
'</tr>';
});
} else {
html = '<tr class="placeholder-row">' +
'<td colspan="4">' +
'No items to display.' +
'</td>' +
'</tr>';
}
table.html(html);
});
}
// remove from invoice table
// TODO: figure out a way to delete rows, and should deleting trade ins be allowed since they count as items on the table?
@ -1601,6 +1710,54 @@ $(function() {
});
});
});
// service charge add
$('#btn-sc-add').click(function(e) {
console.log('adding service charge');
// add dropdown before the button
var html = '<div class="form-group m-form__group row">';
html += '<div class="col-lg-6">';
html += '<div class="col-lg-12 form-group-inner">';
html += '<select class="form-control m-input sc-select" name="service_charges">';
{% for key, sc in service_charges %}
html += '<option value="{{ sc.getID }}" data-amount="{{ sc.getAmount }}">{{ sc.getName }}</option>';
{% endfor %}
html += '</select>';
html += '</div>';
html += '</div>';
html += '<div class="col-lg-5">';
html += '<input class="form-control sc-amount" type="text" value="0" disabled>';
html += '</div>';
html += '<div class="col-lg-1">';
html += '<button class="btn btn-danger btn-sc-remove">X</button>';
html += '</div>';
html += '</div>';
$('#sc-section').append(html);
// trigger change in select
$('#sc-section').find('.sc-select').last().change();
return false;
});
// service charge remove
$('body').on('click', '.btn-sc-remove', function(e) {
console.log('removing service charge');
$(this).closest('.row').remove();
generateInvoice();
return false;
});
$('body').on('change', '.sc-select', function(e) {
var amount = $(this).children('option:selected').data('amount');
$(this).closest('.row').find('.sc-amount').val(amount);
generateInvoice();
});
});
</script>
{% endblock %}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Job Orders (All)
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-12">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div class="input-group">
<select class="form-control m-input" id="rider_list" name="rider_list">
<option value="">All Riders</option>
{% for rider in riders %}
<option value="{{ rider.getID }}">{{ rider.getFirstName ~ ' ' ~ rider.getLastName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div class="input-daterange input-group" id="date-range">
<input role="presentation" type="text" class="form-control m-input" id="date_start" name="date_start" placeholder="Start date" />
<div class="input-group-append">
<span class="input-group-text"><i class="la la-ellipsis-h"></i></span>
</div>
<input role="presentation" type="text" class="form-control" id="date_end" name="date_end" placeholder="End date" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#date-range").datepicker({
orientation: "bottom"
});
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url('jo_all_rows') }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
rows: {
beforeTemplate: function (row, data, index) {
if (data.flag_advance) {
$(row).addClass('m-table__row--danger');
}
}
},
columns: [
{
field: 'id',
title: 'JO Number'
},
{
field: 'plate_number',
title: 'Plate #'
},
{
field: 'car_model',
title: 'Car Model'
},
{
field: 'customer_name',
title: 'Customer'
},
{
field: 'service_type',
title: 'Service Type',
},
{
field: 'delivery_address',
title: 'Customer Area'
},
{
field: 'type',
title: 'Schedule'
},
{
field: 'date_schedule',
title: 'Scheduled Date'
},
{
field: 'rider_name',
title: 'Rider'
},
{
field: 'rider_plate_number',
title: 'Rider Plate #'
},
{
field: 'status',
title: 'Status'
},
{
field: 'processor',
title: 'Dispatcher'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
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"><i class="la la-edit"></i></a>';
actions += '<a href="' + row.meta.pdf_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="PDF" target="_blank"><i class="la la-file-o"></i></a>';
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
// auto refresh table
setInterval(function() {
table.reload();
}, {{ table_refresh_rate }});
$("#rider_list").on("change", function() {
table.search($(this).val(), "rider");
});
$("#date_start").on("change", function() {
var date_start = $(this).val();
var date_end = $("[name='date_end']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
$("#date_end").on("change", function() {
console.log($(this).val());
var date_end = $(this).val();
var date_start = $("[name='date_start']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,183 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Job Orders (Fulfillment)
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-12">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div>
<select class="form-control m-input" id="rider_list" name="rider_list">
<option value="">All Riders</option>
{% for rider in riders %}
<option value="{{ rider.getID }}">{{ rider.getFirstName ~ ' ' ~ rider.getLastName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div class="input-daterange input-group" id="date-range">
<input role="presentation" type="text" class="form-control m-input" id="date_start" name="date_start" placeholder="Start date" />
<div class="input-group-append">
<span class="input-group-text"><i class="la la-ellipsis-h"></i></span>
</div>
<input role="presentation" type="text" class="form-control" id="date_end" name="date_end" placeholder="End date" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#date-range").datepicker({
orientation: "bottom"
});
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url('jo_fulfill_rows') }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
rows: {
beforeTemplate: function (row, data, index) {
if (data.flag_advance) {
$(row).addClass('m-table__row--danger');
}
}
},
columns: [
{
field: 'id',
title: 'JO Number'
},
{
field: 'delivery_address',
title: 'Customer Area'
},
{
field: 'service_type',
title: 'Type of Transaction'
},
{
field: 'type',
title: 'Schedule'
},
{
field: 'date_schedule',
title: 'Scheduled Date'
},
{
field: 'status',
title: 'Status'
},
{
field: 'processor',
title: 'Dispatcher'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var 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-edit" title="View / Edit"><i class="la la-edit"></i></a>';
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
// auto refresh table
setInterval(function() {
table.reload();
}, {{ table_refresh_rate }});
$("#rider_list").on("change", function() {
console.log($(this).val());
table.search($(this).val(), "rider");
});
$("#date_start").on("change", function() {
var date_start = $(this).val();
var date_end = $("[name='date_end']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
$("#date_end").on("change", function() {
console.log($(this).val());
var date_end = $(this).val();
var date_start = $("[name='date_start']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,206 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Job Orders (Open)
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-12">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div>
<select class="form-control m-input" id="rider_list" name="rider_list">
<option value="">All Riders</option>
{% for rider in riders %}
<option value="{{ rider.getID }}">{{ rider.getFirstName ~ ' ' ~ rider.getLastName }} </option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<div class="input-daterange input-group" id="date-range">
<input role="presentation" type="text" class="form-control m-input" id="date_start" name="date_start" placeholder="Start date" />
<div class="input-group-append">
<span class="input-group-text"><i class="la la-ellipsis-h"></i></span>
</div>
<input role="presentation" type="text" class="form-control" id="date_end" name="date_end" placeholder="End date" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#date-range").datepicker({
orientation: "bottom"
});
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url('jo_open_rows') }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
rows: {
beforeTemplate: function (row, data, index) {
if (data.flag_advance) {
$(row).addClass('m-table__row--danger');
}
if (data.is_mobile) {
$(row).addClass('m-table__row--is_mobile');
}
}
},
columns: [
{
field: 'id',
title: 'JO #'
},
{
field: 'plate_number',
title: 'Plate #'
},
{
field: 'car_model',
title: 'Car Model'
},
{
field: 'customer_name',
title: 'Customer'
},
{
field: 'service_type',
title: 'Service Type',
},
{
field: 'delivery_address',
title: 'Area'
},
{
field: 'type',
title: 'Schedule'
},
{
field: 'date_schedule',
title: 'Scheduled Date'
},
{
field: 'rider_name',
title: 'Rider'
},
{
field: 'rider_plate_number',
title: 'Rider Plate #'
},
{
field: 'status',
title: 'Status'
},
{
field: 'processor',
title: 'Dispatcher'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
{% if is_granted('jo_onestep.edit') %}
var 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="Edit"><i class="fa fa-file"></i></a>';
{% endif %}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
// auto refresh table
setInterval(function() {
table.reload();
}, {{ table_refresh_rate }});
$("#rider_list").on("change", function() {
table.search($(this).val(), "rider");
});
$("#date_start").on("change", function() {
var date_start = $(this).val();
var date_end = $("[name='date_end']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
$("#date_end").on("change", function() {
console.log($(this).val());
var date_end = $(this).val();
var date_start = $("[name='date_start']").val();
var date_array = [date_start, date_end];
table.search(date_array, "schedule_date");
});
});
</script>
{% endblock %}

View file

@ -47,6 +47,9 @@
<form id="row-form" class="m-form m-form--fit m-form--label-align-right" method="post" action="{{ submit_url }}">
<input type="hidden" id="invoice-change" name="invoice_change" value="0">
<input type="hidden" id="cid" name="cid" value="0">
{% if mode in ['update-assigning', 'update-processing', 'update-reassign-hub', 'update-reassign-rider', 'update-all', 'open_edit'] %}
<input type="hidden" id="cid" name="cid" value="{{ obj.getCustomer.getID }}">
{% endif %}
<div class="m-portlet__body">
{% if ftags.vehicle_dropdown %}

View file

@ -135,7 +135,7 @@
sortable: false,
overflow: 'visible',
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>' + '<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>';
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>';
return actions;
},

View file

@ -148,9 +148,6 @@
{% 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>';
{% 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;
},

View file

@ -13,7 +13,7 @@
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-8">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
@ -150,6 +150,66 @@
{% endif %}
</div>
</div>
<div class="m-form__seperator m-form__seperator--dashed"></div>
<div class="m-form__section">
<div class="m-form__heading">
<h3 class="m-form__heading-title">
Active Job Orders
</h3>
</div>
<div class="form-group m-form__group row">
<div class="col-lg-12">
<table id="jo-table" class="table m-table">
<thead>
<tr>
<th>ID</th>
<th>Date</th>
<th>Customer</th>
<th>Location</th>
<th>Q Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% set active_jo_id = obj.getActiveJobOrder.getID|default(0) %}
{% for jo in obj.getOpenJobOrders %}
<tr>
<td>{{ jo.getID }}</td>
<td>{{ jo.getDateSchedule.format('Y-m-d H:i:s') }}</td>
<td>{{ jo.getCustomer.getNameDisplay }}</td>
<td>{{ jo.getDeliveryAddress|default('') }}</td>
<td>{% if jo.getID == active_jo_id %}<span class="m-badge m-badge--success m-badge--wide">Active</span>{% endif %}</td>
<td>
<a href="{{ url('rider_priority_up_jo', {'id': obj.getID, 'jo_id': jo.getID}) }}" class="btn m-btn m-btn--icon">
<span>
<i class="fa fa-angle-up"></i>
</span>
</a>
<a href="{{ url('rider_priority_down_jo', {'id': obj.getID, 'jo_id': jo.getID}) }}" class="btn m-btn m-btn--icon">
<span>
<i class="fa fa-angle-down"></i>
</span>
</a>
{% if jo.getID != active_jo_id %}
<!-- TODO: make this submit via ajax post -->
<a href="{{ url('rider_active_jo', {'id': obj.getID, 'jo_id': jo.getID}) }}" class="btn btn-danger m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-check"></i>
</span>
</a>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6">No assigned job orders.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="m-form__seperator m-form__seperator--dashed"></div>
<div class="m-form__section m-form__section--last">
<div class="m-form__heading">

View file

@ -10,4 +10,16 @@
{{ cust.getNameDisplay }}<br>
{{ cv.getPlateNumber }}
{% endif %}
<br>
{% set job_orders = rider.getOpenJobOrders %}
{% if job_orders is not empty %}
{% for job_order in job_orders %}
{% set customer = job_order.getCustomer %}
{% set cust_vehicle = job_order.getCustomerVehicle %}
<br>
<a href="{{ url('jo_onestep_edit_form', {'id': job_order.getID}) }}">Job Order #{{ job_order.getID }}</a><br>
{{ customer.getNameDisplay }}<br>
{{ cust_vehicle.getPlateNumber }}
{% endfor %}
{% endif %}

View file

@ -0,0 +1,143 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">Service Charges</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-8">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__head">
<div class="m-portlet__head-caption">
<div class="m-portlet__head-title">
<span class="m-portlet__head-icon">
<i class="la la-key"></i>
</span>
<h3 class="m-portlet__head-text">
{% if mode == 'update' %}
Edit Service Charge
<small>{{ obj.getID() }}</small>
{% else %}
New Service Charge
{% endif %}
</h3>
</div>
</div>
</div>
<form id="row-form" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ mode == 'update' ? url('service_charge_update_submit', {'id': obj.getID()}) : url('service_charge_create_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row no-border">
<div class="col-lg-12">
<label data-field="name">
Name:
</label>
<input type="text" name="name" class="form-control m-input" value="{{ obj.getName() }}">
<div class="form-control-feedback hide" data-field="name"></div>
</div>
</div>
<div class="form-group m-form__group row no-border">
<div class="col-lg-12">
<label data-field="amount">
Amount:
</label>
<input type="number" name="amount" class="form-control m-input" value="{{ obj.getAmount() }}">
<div class="form-control-feedback hide" data-field="amount"></div>
</div>
</div>
</div>
<div class="m-portlet__foot m-portlet__foot--fit">
<div class="m-form__actions m-form__actions--solid m-form__actions--right">
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-success">Submit</button>
<a href="{{ url('service_charge_list') }}" class="btn btn-secondary">Back</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#row-form").submit(function(e) {
var form = $(this);
e.preventDefault();
$.ajax({
method: "POST",
url: form.prop('action'),
data: form.serialize()
}).done(function(response) {
// remove all error classes
removeErrors();
swal({
title: 'Done!',
text: 'Your changes have been saved!',
type: 'success',
onClose: function() {
window.location.href = "{{ url('service_charge_list') }}";
}
});
}).fail(function(response) {
if (response.status == 422 || response.status == 403) {
var errors = response.responseJSON.errors;
var firstfield = false;
// remove all error classes first
removeErrors();
// display errors contextually
$.each(errors, function(field, msg) {
var formfield = $("[name='" + field + "']");
var label = $("label[data-field='" + field + "']");
var msgbox = $(".form-control-feedback[data-field='" + field + "']");
// add error classes to bad fields
formfield.addClass('form-control-danger');
label.addClass('has-danger');
msgbox.html(msg).addClass('has-danger').removeClass('hide');
// check if this field comes first in DOM
var domfield = formfield.get(0);
if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) {
firstfield = domfield;
}
});
// focus on first bad field
firstfield.focus();
// scroll to above that field to make it visible
$('html, body').animate({
scrollTop: $(firstfield).offset().top - 200
}, 100);
}
});
});
// remove all error classes
function removeErrors() {
$(".form-control-danger").removeClass('form-control-danger');
$("[data-field]").removeClass('has-danger');
$(".form-control-feedback[data-field]").addClass('hide');
}
});
</script>
{% endblock %}

View file

@ -0,0 +1,142 @@
{% extends 'base.html.twig' %}
{% block body %}
<!-- BEGIN: Subheader -->
<div class="m-subheader">
<div class="d-flex align-items-center">
<div class="mr-auto">
<h3 class="m-subheader__title">
Service Charges
</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-12">
<div class="m-portlet m-portlet--mobile">
<div class="m-portlet__body">
<div class="m-form m-form--label-align-right m--margin-top-20 m--margin-bottom-30">
<div class="row align-items-center">
<div class="col-xl-8 order-2 order-xl-1">
<div class="form-group m-form__group row align-items-center">
<div class="col-md-4">
<div class="m-input-icon m-input-icon--left">
<input type="text" class="form-control m-input m-input--solid" placeholder="Search..." id="data-rows-search">
<span class="m-input-icon__icon m-input-icon__icon--left">
<span><i class="la la-search"></i></span>
</span>
</div>
</div>
</div>
</div>
<div class="col-xl-4 order-1 order-xl-2 m--align-right">
<a href="{{ url('service_charge_create') }}" class="btn btn-focus m-btn m-btn--custom m-btn--icon m-btn--air m-btn--pill">
<span>
<i class="la la-key"></i>
<span>New Service Charge</span>
</span>
</a>
<div class="m-separator m-separator--dashed d-xl-none"></div>
</div>
</div>
</div>
<!--begin: Datatable -->
<div id="data-rows"></div>
<!--end: Datatable -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
var options = {
data: {
type: 'remote',
source: {
read: {
url: '{{ url("service_charge_rows") }}',
method: 'POST'
}
},
saveState: {
cookie: false,
webstorage: false
},
pageSize: 10,
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
columns: [
{
field: 'id',
title: 'ID'
},
{
field: 'name',
title: 'Name'
},
{
field: 'Actions',
width: 110,
title: 'Actions',
sortable: false,
overflow: 'visible',
template: function (row, index, datatable) {
var actions = '';
if (row.meta.update_url != '') {
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" data-id="' + row.name + '" title="Edit"><i class="la la-edit"></i></a>';
}
if (row.meta.delete_url != '') {
actions += '<a href="' + row.meta.delete_url + '" class="m-portlet__nav-link btn m-btn m-btn--hover-danger m-btn--icon m-btn--icon-only m-btn--pill btn-delete" data-id="' + row.id + '" title="Delete"><i class="la la-trash"></i></a>';
}
return actions;
},
}
],
search: {
onEnter: false,
input: $('#data-rows-search'),
delay: 400
}
};
var table = $("#data-rows").mDatatable(options);
$(document).on('click', '.btn-delete', function(e) {
var url = $(this).prop('href');
var id = $(this).data('id');
var btn = $(this);
e.preventDefault();
swal({
title: 'Confirmation',
html: 'Are you sure you want to delete <strong>' + id + '</strong>?',
type: 'warning',
showCancelButton: true
}).then((result) => {
if (result.value) {
$.ajax({
method: "DELETE",
url: url
}).done(function(response) {
table.row(btn.parents('tr')).remove();
table.reload();
});
}
});
});
});
</script>
{% endblock %}

View file

@ -11,7 +11,7 @@ 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.'
delivery_instructions_label: 'CarFix Details'
# images
image_logo_login: /assets/images/black-text-logo-01.png

View file

@ -11,7 +11,7 @@ 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.'
delivery_instructions_label: 'CarFix Details'
# images
image_logo_login: /assets/images/black-text-logo-01.png
@ -22,8 +22,6 @@ 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: 14.6091
default_long: 121.0223
#default_lat: 3.084216
#default_long: 101.6129996
default_region: ph
default_lat: 3.084216
default_long: 101.6129996
default_region: my