Merge branch '379-resq-jo-and-jo-event-report' into 'master'

Resolve "Resq - JO and JO Event Report"

Closes #379

See merge request jankstudio/resq!424
This commit is contained in:
Kendrick Chan 2020-04-17 10:32:26 +00:00
commit 7dee59782a
9 changed files with 585 additions and 6 deletions

View file

@ -326,6 +326,10 @@ access_keys:
label: Vehicle Battery Compatibility Report label: Vehicle Battery Compatibility Report
- id: report.warranty.details - id: report.warranty.details
label: Warranty Details Report label: Warranty Details Report
- id: report.jo.details
label: Job Order Details Report
- id: report.jo_events
label: Job Order Events Report
- id: service - id: service
label: Other Services label: Other Services

View file

@ -77,3 +77,23 @@ rep_warranty_details_export_csv:
path: /report/warranty_details_report path: /report/warranty_details_report
controller: App\Controller\ReportController::warrantyDetailsExportCSV controller: App\Controller\ReportController::warrantyDetailsExportCSV
methods: [POST] methods: [POST]
rep_jo_details_form:
path: /report/jo_details_report
controller: App\Controller\ReportController::jobOrderDetailsForm
methods: [GET]
rep_jo_details_submit:
path: /report/jo_details_report
controller: App\Controller\ReportController::jobOrderDetailsSubmit
methods: [POST]
rep_jo_events_form:
path: /report/jo_events_report
controller: App\Controller\ReportController::jobOrderEventsForm
methods: [GET]
rep_jo_events_submit:
path: /report/jo_events_report
controller: App\Controller\ReportController::jobOrderEventsSubmit
methods: [POST]

View file

@ -1,14 +1,16 @@
class DashboardMap { class DashboardMap {
constructor(options, rider_markers, cust_markers) { constructor(options, rider_markers, cust_markers, mc_markers) {
this.options = options; this.options = options;
this.rider_markers = rider_markers; this.rider_markers = rider_markers;
this.cust_markers = cust_markers; this.cust_markers = cust_markers;
this.mobile_cust_markers = mc_markers;
// layer groups // layer groups
this.layer_groups = { this.layer_groups = {
'rider_available': L.layerGroup(), 'rider_available': L.layerGroup(),
'rider_active_jo': L.layerGroup(), 'rider_active_jo': L.layerGroup(),
'customer': L.layerGroup() 'customer': L.layerGroup(),
'mobile_customer': L.layerGroup(),
}; };
} }
@ -31,6 +33,7 @@ class DashboardMap {
this.layer_groups.rider_available.addTo(this.map); this.layer_groups.rider_available.addTo(this.map);
this.layer_groups.rider_active_jo.addTo(this.map); this.layer_groups.rider_active_jo.addTo(this.map);
this.layer_groups.customer.addTo(this.map); this.layer_groups.customer.addTo(this.map);
this.layer_groups.mobile_customer.addTo(this.map);
// base layer // base layer
var baseMaps = { var baseMaps = {
@ -42,7 +45,8 @@ class DashboardMap {
var overlayMaps = { var overlayMaps = {
'Available Riders' : this.layer_groups.rider_available, 'Available Riders' : this.layer_groups.rider_available,
'JO Riders' : this.layer_groups.rider_active_jo, 'JO Riders' : this.layer_groups.rider_active_jo,
'Customers' : this.layer_groups.customer 'Customers' : this.layer_groups.customer,
'Mobile Customers': this.layer_groups.mobile_customer
} }
L.control.layers(baseMaps, overlayMaps).addTo(this.map); L.control.layers(baseMaps, overlayMaps).addTo(this.map);
@ -133,6 +137,32 @@ class DashboardMap {
layer_group.removeLayer(markers[id]); layer_group.removeLayer(markers[id]);
} }
putMobileCustomerMarker(id, lat, lng) {
this.putMarker(
id,
lat,
lng,
this.mobile_cust_markers,
this.options.icons.mobile_customer,
this.layer_groups.mobile_customer,
this.options.cust_popup_url
);
}
removeMobileCustomerMarker(id) {
console.log('removing mobile customer marker for ' + id);
var layer_group = this.layer_groups.mobile_customer;
var markers = this.mobile_cust_markers;
// no customer marker with that id
if (!markers.hasOwnProperty(id)) {
console.log('no such marker to remove');
return;
}
layer_group.removeLayer(markers[id]);
}
putRiderAvailableMarker(id, lat, lng) { putRiderAvailableMarker(id, lat, lng) {
this.putMarker( this.putMarker(
id, id,
@ -180,10 +210,12 @@ class DashboardMap {
my.layer_groups.rider_available.clearLayers(); my.layer_groups.rider_available.clearLayers();
my.layer_groups.rider_active_jo.clearLayers(); my.layer_groups.rider_active_jo.clearLayers();
my.layer_groups.customer.clearLayers(); my.layer_groups.customer.clearLayers();
my.layer_groups.mobile_customer.clearLayers();
// get riders and job orders // get riders and job orders
var riders = response.riders; var riders = response.riders;
var jos = response.jos; var jos = response.jos;
var mobile_jos = response.mobile_jos;
// job orders // job orders
$.each(jos, function(id, data) { $.each(jos, function(id, data) {
@ -193,6 +225,14 @@ class DashboardMap {
my.putCustomerMarker(id, lat, lng); my.putCustomerMarker(id, lat, lng);
}); });
// mobile app job orders
$.each(mobile_jos, function(id, data) {
var lat = data.latitude;
var lng = data.longitude;
my.putMobileCustomerMarker(id, lat, lng);
});
// riders // riders
$.each(riders, function(id, data) { $.each(riders, function(id, data) {
var lat = data.latitude; var lat = data.latitude;

View file

@ -13,7 +13,9 @@ use App\Service\JobOrderCache;
use App\Service\RiderCache; use App\Service\RiderCache;
use App\Entity\Rider; use App\Entity\Rider;
use App\Entity\JobOrder;
use App\Ramcar\TransactionOrigin;
class HomeController extends Controller class HomeController extends Controller
{ {
@ -63,6 +65,24 @@ class HomeController extends Controller
$riders[$rider_id]['has_jo'] = true; $riders[$rider_id]['has_jo'] = true;
} }
// get JOs with transaction origin TransactionOrigin::MOBILE_APP from list of active_jos
$mobile_jos = [];
foreach ($active_jos as $jo_id => $jo_data)
{
$jo = $em->getRepository(JobOrder::class)->find($jo_id);
if ($jo == null)
{
unset($active_jos[$jo_id]);
continue;
}
if ($jo->getSource() == TransactionOrigin::MOBILE_APP)
{
$mobile_jos[$jo_id] = $jo_data;
unset($active_jos[$jo_id]);
}
}
// get active riders from cache // get active riders from cache
// get all riders // get all riders
/* /*
@ -120,6 +140,7 @@ class HomeController extends Controller
return $this->json([ return $this->json([
'jos' => $active_jos, 'jos' => $active_jos,
'riders' => $riders, 'riders' => $riders,
'mobile_jos' => $mobile_jos,
]); ]);
} }

View file

@ -6,10 +6,12 @@ use App\Ramcar\JORejectionReason;
use App\Ramcar\ServiceType; use App\Ramcar\ServiceType;
use App\Ramcar\JOStatus; use App\Ramcar\JOStatus;
use App\Ramcar\InvalidPlateNumber; use App\Ramcar\InvalidPlateNumber;
use App\Ramcar\JOEventType;
use App\Entity\JORejection; use App\Entity\JORejection;
use App\Entity\Battery; use App\Entity\Battery;
use App\Entity\JobOrder; use App\Entity\JobOrder;
use App\Entity\JOEvent;
use App\Entity\Warranty; use App\Entity\Warranty;
use App\Entity\CustomerVehicle; use App\Entity\CustomerVehicle;
use App\Entity\MobileSession; use App\Entity\MobileSession;
@ -725,6 +727,116 @@ class ReportController extends Controller
} }
/**
* @Menu(selected="outlet_list")
*/
public function jobOrderDetailsForm()
{
$this->denyAccessUnlessGranted('report.jo.details', null, 'No access.');
return $this->render('report/jo-details/form.html.twig');
}
public function jobOrderDetailsSubmit(Request $req, EntityManagerInterface $em)
{
$data = $this->getJobOrderDetails($req, $em);
$resp = new StreamedResponse();
$resp->setCallback(function() use ($data) {
// csv output
$csv_handle = fopen('php://output', 'w+');
fputcsv($csv_handle, [
'Job Order ID',
'Dispatcher',
'Coordinate Latitude',
'Coordinate Longitude',
'Date and Time Hub Assigned for JO',
'Date Hub Assigned for JO',
'Time Hub Assigned for JO',
'Date and Time Rider Assigned for JO',
'Date Rider Assigned for JO',
'Time Rider Assigned for JO',
'Date and Time Created for Invoice',
'Date and Time Created for JO',
'Date Created for JO',
'Time Created for JO',
'Outlet Name',
'Rider Name',
'Service Type',
'Source',
'Status',
'SKU',
]);
foreach ($data as $row)
{
fputcsv($csv_handle, $row);
}
fclose($csv_handle);
});
$filename = 'job_order_details_report' . '.csv';
$resp->setStatusCode(200);
$resp->headers->set('Content-Type', 'text/csv; charset=utf-8');
$resp->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
return $resp;
}
/**
* @Menu(selected="outlet_list")
*/
public function jobOrderEventsForm()
{
$this->denyAccessUnlessGranted('report.jo_events', null, 'No access.');
return $this->render('report/jo-events/form.html.twig');
}
public function jobOrderEventsSubmit(Request $req, EntityManagerInterface $em)
{
$hub_events = $this->getJobOrderEvents($req, JOEventType::HUB_ASSIGN);
$arrive_events = $this->getJobOrderEvents($req, JOEventType::RIDER_ARRIVE);
$accept_events = $this->getJobOrderEvents($req, JOEventType::RIDER_ACCEPT);
$blanks = [];
array_push($blanks, array("\t","\t","\t","\t"));
$data = array_merge($hub_events, $blanks, $arrive_events, $blanks, $accept_events);
$resp = new StreamedResponse();
$resp->setCallback(function() use ($data) {
// csv output
$csv_handle = fopen('php://output', 'w+');
fputcsv($csv_handle, [
'Job Order ID',
'Date and Time Created for JO',
'Date Created for JO',
'Time Created for JO',
'Service Type',
'Type ID',
]);
// write hub events
foreach ($data as $row)
{
fputcsv($csv_handle, $row);
}
fclose($csv_handle);
});
$filename = 'job_order_events_report' . '.csv';
$resp->setStatusCode(200);
$resp->headers->set('Content-Type', 'text/csv; charset=utf-8');
$resp->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
return $resp;
}
protected function processPopappFile(UploadedFile $csv_file, EntityManagerInterface $em) protected function processPopappFile(UploadedFile $csv_file, EntityManagerInterface $em)
{ {
// attempt to open file // attempt to open file
@ -1162,4 +1274,195 @@ class ReportController extends Controller
return $bsize_hash; return $bsize_hash;
} }
protected function getJobOrderDetails(Request $req, EntityManagerInterface $em)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(JobOrder::class)
->createQueryBuilder('r');
// get dates
$raw_date_start = $req->request->get('date_start');
$raw_date_end = $req->request->get('date_end');
$date_start = DateTime::createFromFormat('m/d/Y', $raw_date_start);
$date_end = DateTime::createFromFormat('m/d/Y', $raw_date_end);
// build query
$query = $qb->where('r.date_create >= :start')
->andWhere('r.date_create <= :end')
->setParameter('start', $date_start->format('Y-m-d') . ' 00:00:00')
->setParameter('end', $date_end->format('Y-m-d') . ' 23:59:59')
->getQuery();
// run query
$jos = $query->getResult();
$result = [];
foreach($jos as $jo)
{
// get dispatcher if any
$dispatcher_name = '';
$dispatcher = $jo->getProcessedBy();
if ($dispatcher != null)
$dispatcher_name = $dispatcher->getFullName();
$coord_long = $jo->getCoordinates()->getLongitude();
$coord_lat = $jo->getCoordinates()->getLatitude();
// find date and time when JO was assigned a hub
$datetime_hub_assign_jo = '';
$date_hub_assign_jo = '';
$time_hub_assign_jo = '';
$hub_assign_event_type = JOEventType::HUB_ASSIGN;
$hub_assign_events = $em->getRepository(JOEvent::class)->findBy(['job_order' => $jo, 'type_id'=> $hub_assign_event_type], ['date_happen' => 'DESC']);
if ($hub_assign_events != null)
{
// TODO: what happens if JO was reassigned a hub multiple times?
// right now, this gets the last time hub was assigned.
foreach ($hub_assign_events as $hub_assign_event)
{
$datetime_hub_assign_jo = $hub_assign_event->getDateHappen()->format('m-d-Y H:i');
$date_hub_assign_jo = $hub_assign_event->getDateHappen()->format('m-d-Y');
$time_hub_assign_jo = $hub_assign_event->getDateHappen()->format('H:i');
}
}
// find date and time when JO was assigned a rider
$datetime_rider_assign_jo = '';
$date_rider_assign_jo = '';
$time_rider_assign_jo = '';
$rider_assign_event_type = JOEventType::RIDER_ASSIGN;
$rider_assign_events = $em->getRepository(JOEvent::class)->findBy(['job_order' => $jo, 'type_id'=> $rider_assign_event_type], ['date_happen' => 'DESC']);
if ($rider_assign_events != null)
{
foreach ($rider_assign_events as $rider_assign_event)
{
// TODO: what happens if JO was reassigned a rider multiple times?
// right now, this gets the last time rider was assigned.
$datetime_rider_assign_jo = $rider_assign_event->getDateHappen()->format('m-d-Y H:i');
$date_rider_assign_jo = $rider_assign_event->getDateHappen()->format('m-d-Y');
$time_rider_assign_jo = $rider_assign_event->getDateHappen()->format('H:i');
}
}
// get invoice date create and item sku if any
$datetime_create_invoice = '';
$sku = '';
$invoice = $jo->getInvoice();
if ($invoice != null)
{
$invoice_create = $invoice->getDateCreate();
$datetime_create_invoice = $invoice_create->format('m-d-Y H:i');
// get item sku
$invoice_items = $invoice->getItems();
foreach ($invoice_items as $item)
{
$battery = $item->getBattery();
if ($battery != null)
$sku = $battery->getModel()->getName() . ' ' . $battery->getSize()->getName();
}
}
// get jo date create
$datetime_create_jo = '';
$date_create_jo = '';
$time_create_jo = '';
$jo_create = $jo->getDateCreate();
$datetime_create_jo = $jo_create->format('m-d-Y H:i');
$date_create_jo = $jo_create->format('m-d-Y');
$time_create_jo = $jo_create->format('H:i');
// get hub if any
$hub_name = '';
$hub = $jo->getHub();
if ($hub != null)
$hub_name = $hub->getName();
// get rider if any
$rider_name = '';
$rider = $jo->getRider();
if ($rider != null)
$rider_name = $rider->getFullName();
$result[] = [
$jo->getID(),
$dispatcher_name,
$coord_lat,
$coord_long,
$datetime_hub_assign_jo,
$date_hub_assign_jo,
$time_hub_assign_jo,
$datetime_rider_assign_jo,
$date_rider_assign_jo,
$time_rider_assign_jo,
$datetime_create_invoice,
$datetime_create_jo,
$date_create_jo,
$time_create_jo,
$hub_name,
$rider_name,
$jo->getServiceType(),
$jo->getSource(),
$jo->getStatus(),
$sku,
];
}
return $result;
}
protected function getJobOrderEvents(Request $req, $event_type)
{
// get query builder
$qb = $this->getDoctrine()
->getRepository(JOEvent::class)
->createQueryBuilder('r');
// get dates
$raw_date_start = $req->request->get('date_start');
$raw_date_end = $req->request->get('date_end');
$date_start = DateTime::createFromFormat('m/d/Y', $raw_date_start);
$date_end = DateTime::createFromFormat('m/d/Y', $raw_date_end);
// build query
$query = $qb->where('r.date_create >= :start')
->andWhere('r.date_create <= :end')
->andWhere('r.type_id = :event_type')
->setParameter('start', $date_start->format('Y-m-d') . ' 00:00:00')
->setParameter('end', $date_end->format('Y-m-d') . ' 23:59:59')
->setParameter('event_type', $event_type)
->getQuery();
// run query
$jo_events = $query->getResult();
$result = [];
foreach ($jo_events as $jo_event)
{
$datetime_jo_create = '';
$date_jo_create = '';
$time_jo_create = '';
$jo = $jo_event->getJobOrder();
$jo_date_create = $jo->getDateCreate();
$datetime_jo_create = $jo_date_create->format('m-d-Y H:i');
$date_jo_create = $jo_date_create->format('m-d-Y');
$time_jo_create = $jo_date_create->format('H:i');
$result[] = [
$jo->getID(),
$datetime_jo_create,
$date_jo_create,
$time_jo_create,
$jo->getServiceType(),
$jo_event->getTypeID(),
];
}
return $result;
}
} }

View file

@ -142,6 +142,26 @@
</span> </span>
</a> </a>
</li> </li>
<li class="m-menu__item " data-redirect="true" aria-haspopup="true">
<a href= "{{ url('rep_jo_details_form') }}" class="m-menu__link">
<i class="m-menu__link-bullet m-menu__link-bullet--dot">
<span></span>
</i>
<span class="m-menu__link-text">
Job Order Details Report
</span>
</a>
</li>
<li class="m-menu__item " data-redirect="true" aria-haspopup="true">
<a href= "{{ url('rep_jo_events_form') }}" class="m-menu__link">
<i class="m-menu__link-bullet m-menu__link-bullet--dot">
<span></span>
</i>
<span class="m-menu__link-text">
Job Order Events Report
</span>
</a>
</li>
</ul> </ul>
</li> </li>
<li class="m-menu__item"> <li class="m-menu__item">

View file

@ -20,7 +20,7 @@
{% endif %} {% endif %}
<script> <script>
function initMap(r_markers, c_markers, icons) { function initMap(r_markers, c_markers, mc_markers, icons) {
var default_lat = {% trans %}default_lat{% endtrans %}; var default_lat = {% trans %}default_lat{% endtrans %};
var default_lng = {% trans %}default_long{% endtrans %}; var default_lng = {% trans %}default_long{% endtrans %};
@ -38,7 +38,7 @@ function initMap(r_markers, c_markers, icons) {
'icons': icons 'icons': icons
}; };
var dashmap = new DashboardMap(options, r_markers, c_markers); var dashmap = new DashboardMap(options, r_markers, c_markers, mc_markers);
dashmap.initialize(); dashmap.initialize();
dashmap.loadLocations('{{ path('rider_locations') }}'); dashmap.loadLocations('{{ path('rider_locations') }}');
@ -81,13 +81,20 @@ var icons = {
html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>", html: "<div style='background-color:#0055FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42], iconSize: [39, 42],
iconAnchor: [15, 42] iconAnchor: [15, 42]
}),
'mobile_customer': L.divIcon({
className: 'map-div-icon',
html: "<div style='background-color:#A200FF;' class='marker-pin'></div><i class='fa fa-user awesome'>",
iconSize: [39, 42],
iconAnchor: [15, 42]
}) })
}; };
var r_markers = {}; var r_markers = {};
var c_markers = {}; var c_markers = {};
var mc_markers = {};
var dashmap = initMap(r_markers, c_markers, icons); var dashmap = initMap(r_markers, c_markers, mc_markers, icons);
initEventHandler(dashmap, icons); initEventHandler(dashmap, icons);
{% endif %} {% endif %}

View file

@ -0,0 +1,82 @@
{% 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 Order Details Report</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<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="fa fa-calendar"></i>
</span>
<h3 class="m-portlet__head-text">
Select a date range
</h3>
</div>
</div>
</div>
<form id="row-form" autocomplete="off" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ url('rep_jo_details_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row">
<div class="input-daterange input-group" id="date-range">
<input role="presentation" type="text" class="form-control m-input" 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" name="date_end" placeholder="End date" />
</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>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#date-range").datepicker({
orientation: "bottom"
});
$("#row-form").submit(function(e) {
var form = $(this);
if (!$("[name='date_start']").val() || !$("[name='date_end']").val()) {
e.preventDefault();
swal({
title: 'Whoops!',
text: 'Please fill in both date fields.',
type: 'warning'
});
return false;
}
});
});
</script>
{% endblock %}

View file

@ -0,0 +1,82 @@
{% 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 Order Events Report</h3>
</div>
</div>
</div>
<!-- END: Subheader -->
<div class="m-content">
<!--Begin::Section-->
<div class="row">
<div class="col-xl-6">
<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="fa fa-calendar"></i>
</span>
<h3 class="m-portlet__head-text">
Select a date range
</h3>
</div>
</div>
</div>
<form id="row-form" autocomplete="off" class="m-form m-form--fit m-form--label-align-right m-form--group-seperator-dashed" method="post" action="{{ url('rep_jo_events_submit') }}">
<div class="m-portlet__body">
<div class="form-group m-form__group row">
<div class="input-daterange input-group" id="date-range">
<input role="presentation" type="text" class="form-control m-input" 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" name="date_end" placeholder="End date" />
</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>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
$(function() {
$("#date-range").datepicker({
orientation: "bottom"
});
$("#row-form").submit(function(e) {
var form = $(this);
if (!$("[name='date_start']").val() || !$("[name='date_end']").val()) {
e.preventDefault();
swal({
title: 'Whoops!',
text: 'Please fill in both date fields.',
type: 'warning'
});
return false;
}
});
});
</script>
{% endblock %}