Merge branch '381-resq-auto-assign-with-advance-order' into 'master'

Resolve "Resq - auto-assign with advance order"

Closes #381

See merge request jankstudio/resq!426
This commit is contained in:
Kendrick Chan 2020-04-21 04:40:48 +00:00
commit 5741d891df
4 changed files with 514 additions and 95 deletions

View file

@ -989,117 +989,209 @@ class APIController extends Controller implements LoggedController
$invoice = $ic->generateInvoice($icrit);
$jo->setInvoice($invoice);
// assign hub and rider
if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY))
// check if advance order
// check for time when request came in
$request_time_in_int = time();
$start = '17:00';
$end = '08:00';
$start_time = strtotime($start);
$end_time = strtotime($end);
if (($request_time_in_int > $start_time) ||
(($request_time_in_int < $start_time) && ($request_time_in_int < $end_time)))
{
// get nearest hub
// $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im);
$nearest_hub = $this->findNearestHub($jo, $em, $map_tools);
// advance order
// assign hub and rider
if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY))
{
// get nearest hub
// $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im);
// TODO: when the inventory manager kicks in, create new function that finds the
// the nearest hub with inventory and rider slot
// convert to DateTime
$nearest_hub = $this->findAdvanceNearestHub($jo, $request_time_in_int, $em, $map_tools);
}
else
{
$nearest_hub = $this->findAdvanceNearestHub($jo, $request_time_in_int, $em, $map_tools);
}
if (!empty($nearest_hub))
{
$jo->setHub($nearest_hub);
$jo->setStatus(JOStatus::ASSIGNED);
}
$em->persist($jo);
$em->persist($invoice);
// add event log for JO
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::CREATE)
->setJobOrder($jo);
$em->persist($event);
// check JO status
if ($jo->getStatus() == JOStatus::ASSIGNED)
{
// add event logs for hub and rider assignments
$hub_assign_event = new JOEvent();
$hub_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::HUB_ASSIGN)
->setJobOrder($jo);
$em->persist($hub_assign_event);
}
$em->flush();
// make invoice json data
$invoice_data = [
'total_price' => $invoice->getTotalPrice(),
'vat_ex_price' => $invoice->getVATExclusivePrice(),
'vat' => $invoice->getVAT(),
'discount' => $invoice->getDiscount(),
'trade_in' => $invoice->getTradeIn(),
];
$items = $invoice->getItems();
$items_data = [];
foreach ($items as $item)
{
$items_data[] = [
'title' => $item->getTitle(),
'qty' => $item->getQuantity() + 0,
'price' => $item->getPrice() + 0.0,
];
}
$invoice_data['items'] = $items_data;
// make job order data
$data = [
'jo_id' => $jo->getID(),
'invoice' => $invoice_data
];
// set data
$res->setData($data);
}
else
{
$nearest_hub = $this->findNearestHub($jo, $em, $map_tools);
}
if (!empty($nearest_hub))
{
// assign rider
$available_riders = $nearest_hub->getAvailableRiders();
if (count($available_riders) > 0)
// assign hub and rider
if (($jo->getServiceType() == ServiceType::BATTERY_REPLACEMENT_NEW) ||
($jo->getServicetype() == ServiceType::BATTERY_REPLACEMENT_WARRANTY))
{
$assigned_rider = null;
if (count($available_riders) > 1)
{
// TODO: the setting of riders into an array
// will no longer be necessary when the contents
// of randomizeRider changes
$riders = [];
foreach ($available_riders as $rider)
{
$riders[] = $rider;
}
$assigned_rider = $this->randomizeRider($riders);
}
else
$assigned_rider = $available_riders[0];
$jo->setHub($nearest_hub);
$jo->setRider($assigned_rider);
$jo->setStatus(JOStatus::ASSIGNED);
$assigned_rider->setAvailable(false);
// get nearest hub
// $nearest_hub = $this->findNearestHubWithInventory($jo, $batt, $em, $map_tools, $im);
$nearest_hub = $this->findNearestHub($jo, $em, $map_tools);
}
else
{
$nearest_hub = $this->findNearestHub($jo, $em, $map_tools);
}
}
$em->persist($jo);
$em->persist($invoice);
if (!empty($nearest_hub))
{
// assign rider
$available_riders = $nearest_hub->getAvailableRiders();
if (count($available_riders) > 0)
{
$assigned_rider = null;
if (count($available_riders) > 1)
{
// TODO: the setting of riders into an array
// will no longer be necessary when the contents
// of randomizeRider changes
$riders = [];
foreach ($available_riders as $rider)
{
$riders[] = $rider;
}
// add event log for JO
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::CREATE)
->setJobOrder($jo);
$em->persist($event);
$assigned_rider = $this->randomizeRider($riders);
}
else
$assigned_rider = $available_riders[0];
$jo->setHub($nearest_hub);
$jo->setRider($assigned_rider);
$jo->setStatus(JOStatus::ASSIGNED);
// check JO status
if ($jo->getStatus() == JOStatus::ASSIGNED)
{
// add event logs for hub and rider assignments
$hub_assign_event = new JOEvent();
$hub_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::HUB_ASSIGN)
$assigned_rider->setAvailable(false);
}
}
$em->persist($jo);
$em->persist($invoice);
// add event log for JO
$event = new JOEvent();
$event->setDateHappen(new DateTime())
->setTypeID(JOEventType::CREATE)
->setJobOrder($jo);
$em->persist($event);
$em->persist($hub_assign_event);
// check JO status
if ($jo->getStatus() == JOStatus::ASSIGNED)
{
// add event logs for hub and rider assignments
$hub_assign_event = new JOEvent();
$hub_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::HUB_ASSIGN)
->setJobOrder($jo);
$rider_assign_event = new JOEvent();
$rider_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::RIDER_ASSIGN)
->setJobOrder($jo);
$em->persist($hub_assign_event);
$em->persist($rider_assign_event);
$rider_assign_event = new JOEvent();
$rider_assign_event->setDateHappen(new DateTime())
->setTypeID(JOEventType::RIDER_ASSIGN)
->setJobOrder($jo);
// user mqtt event
$payload = [
'event' => 'outlet_assign'
$em->persist($rider_assign_event);
// user mqtt event
$payload = [
'event' => 'outlet_assign'
];
$mclient->sendEvent($jo, $payload);
$rah->assignJobOrder($jo, $jo->getRider());
}
$em->flush();
// make invoice json data
$invoice_data = [
'total_price' => $invoice->getTotalPrice(),
'vat_ex_price' => $invoice->getVATExclusivePrice(),
'vat' => $invoice->getVAT(),
'discount' => $invoice->getDiscount(),
'trade_in' => $invoice->getTradeIn(),
];
$mclient->sendEvent($jo, $payload);
$items = $invoice->getItems();
$items_data = [];
foreach ($items as $item)
{
$items_data[] = [
'title' => $item->getTitle(),
'qty' => $item->getQuantity() + 0,
'price' => $item->getPrice() + 0.0,
];
}
$invoice_data['items'] = $items_data;
$rah->assignJobOrder($jo, $jo->getRider());
}
$em->flush();
// make invoice json data
$invoice_data = [
'total_price' => $invoice->getTotalPrice(),
'vat_ex_price' => $invoice->getVATExclusivePrice(),
'vat' => $invoice->getVAT(),
'discount' => $invoice->getDiscount(),
'trade_in' => $invoice->getTradeIn(),
];
$items = $invoice->getItems();
$items_data = [];
foreach ($items as $item)
{
$items_data[] = [
'title' => $item->getTitle(),
'qty' => $item->getQuantity() + 0,
'price' => $item->getPrice() + 0.0,
// make job order data
$data = [
'jo_id' => $jo->getID(),
'invoice' => $invoice_data
];
// set data
$res->setData($data);
}
$invoice_data['items'] = $items_data;
// make job order data
$data = [
'jo_id' => $jo->getID(),
'invoice' => $invoice_data
];
// set data
$res->setData($data);
return $res->getReturnResponse();
}
@ -2464,4 +2556,306 @@ class APIController extends Controller implements LoggedController
return $selected_rider;
}
protected function findAdvanceNearestHub($jo, $request_time, EntityManagerInterface $em, MapTools $map_tools)
{
// get the nearest 10 hubs
$selected_hub = null;
$hubs = $map_tools->getClosestOpenHubs($jo->getCoordinates(), 10);
$nearest_hubs_with_distance = [];
$nearest_branch_codes = [];
foreach ($hubs as $hub)
{
$nearest_hubs_with_distance[] = $hub;
//if (!empty($hub['hub']->getBranchCode()))
// $nearest_branch_codes[] = $hub['hub']->getBranchCode();
}
// check if nearest hubs have branch codes
//if (count($nearest_branch_codes) == 0)
// return $selected_hub;
// assume all 10 have stock
// find the nearest hub with available riders
$nearest = null;
$slot_found = false;
foreach ($nearest_hubs_with_distance as $nhd)
{
if (empty($nearest))
$nearest = $nhd;
else
{
if ($nhd['distance'] < $nearest['distance'])
$nearest = $nhd;
}
$result = $this->findRiderSlots($jo, $nearest['hub'], $request_time, $em);
if ($result != null)
$slot_found = $result['slot_found'];
else
$nearest = null;
}
if ($slot_found && $nearest != null)
{
$result = $this->findRiderSlots($jo, $nearest['hub'], $request_time, $em);
if ($result != null)
{
$jo_date_schedule = $result['date_schedule'];
$jo->setAdvanceOrder(true);
$jo->setDateSchedule($jo_date_schedule);
$selected_hub = $nearest['hub'];
}
}
// uncomment this snippet when inventory check becomes active
// get battery sku
/*
if ($batt != null)
{
$skus[] = $batt->getSAPCode();
// api call to check inventory
// pass the list of branch codes of nearest hubs and the skus
// go through returned list of branch codes
// bypass inventory check for now
// $hubs_with_inventory = $im->getBranchesInventory($nearest_branch_codes, $skus);
if (!empty($hubs_with_inventory))
{
$nearest = [];
$flag_hub_found = false;
foreach ($hubs_with_inventory as $hub_with_inventory)
{
// find hub according to branch code
$found_hub = $em->getRepository(Hub::class)->findOneBy(['branch_code' => $hub_with_inventory['BranchCode']]);
if ($found_hub != null)
{
// check rider availability
if (count($found_hub->getAvailableRiders()) > 0)
{
// check against nearest hubs with distance
foreach ($nearest_hubs_with_distance as $nhd)
{
// get distance of hub from location, compare with $nearest. if less, replace nearest
if ($found_hub->getID() == $nhd['hub']->getID())
{
if (empty($nearest))
{
$nearest = $nhd;
$flag_hub_found = true;
}
else
{
if ($nhd['distance'] < $nearest['distance'])
{
$nearest = $nhd;
$flag_hub_found = true;
}
}
}
}
}
}
}
$selected_hub = $nearest['hub'];
}
} */
return $selected_hub;
}
protected function findRiderSlots($jo, Hub $hub, $request_time, EntityManagerInterface $em)
{
$flag_found_slot = false;
$results = [];
// array of # of riders that can handle JOs in a timeslot
$hub_rider_slots = [];
// populate the array with the hub's rider slots
for ($i = 0; $i <=7; $i++)
{
$hub_rider_slots[$i] = $hub->getRiderSlots();
}
// check hub's advance orders for the day
// get number of advance orders for the next day if request came in before midnight
// or for current day if request came in after midnight
// check request_time
$midnight = strtotime('00:00');
$start_date = new DateTime();
$end_date = new DateTime();
if ($request_time < $midnight)
{
// add +1 to date to start and end date
$start_date->add(new DateInterval('P1D'));
$end_date->add(new DateInterval('P1D'));
}
// set time bounds for the start and end date
$start_date->setTime(0, 1);
$end_date->setTime(23, 59);
$str_request_time = date('Y-m-d H:i:s', $request_time);
$time_of_request = DateTime::createFromFormat('Y-m-d H:i:s', $str_request_time);
// NOTE: get advance orders via query
// get JOs assigned to hub that are advance orders and scheduled for the next day(if before midnight) or current day(if after midnight) with
// for hub assignment status
$query = $em->createQuery('select jo from App\Entity\JobOrder jo where jo.hub = :hub and jo.flag_advance = true and
jo.date_schedule >= :date_start and jo.date_schedule <= :date_end and jo.status = :status');
$jos_advance_orders = $query->setParameters([
'hub' => $hub,
'date_start' => $start_date,
'date_end' => $end_date,
'status' => JOStatus::ASSIGNED,
])
->getResult();
// check each JO's date_schedule, decrement rider_slots if date schedule falls in that slot
// index - equivalent time
// 0 - 8-9
// 1 - 9-10
// 2 - 10-11
// 3 - 11-12
// 4 - 12 -13
// 5 - 13-14
// 6 - 14-15
// 7 - 15-16
foreach ($jos_advance_orders as $advance_jo)
{
// get time schedule
$date_schedule = $advance_jo->getDateSchedule();
$time_schedule = $date_schedule->format('H:i');
$hour_schedule = $date_schedule->format('H');
$minute_schedule = $date_schedule->format('i');
// check minutes. for now, if not 00, take up current slot and next one
// if hour fits in the last slot but minutes is not 00, move to the next available hub
// TODO: need to add custom minutes threshold
if ($minute_schedule != '00')
{
switch($hour_schedule) {
case '8':
$hub_rider_slots[0]--;
$hub_rider_slots[1]--;
break;
case '9':
$hub_rider_slots[1]--;
$hub_rider_slots[2]--;
break;
case '10':
$hub_rider_slots[2]--;
$hub_rider_slots[3]--;
break;
case '11':
$hub_rider_slots[3]--;
$hub_rider_slots[4]--;
break;
case '12':
$hub_rider_slots[4]--;
$hub_rider_slots[5]--;
break;
case '13':
$hub_rider_slots[5]--;
$hub_rider_slots[6]--;
break;
case '14':
$hub_rider_slots[6]--;
$hub_rider_slots[7]--;
break;
case '15':
error_log('No slots for the day');
break;
default:
error_log('Does not fit in any time slot. ' . $time_schedule);
}
}
else
{
switch ($hour_schedule) {
case '8':
$hub_rider_slots[0]--;
break;
case '9':
$hub_rider_slots[1]--;
break;
case '10':
$hub_rider_slots[2]--;
break;
case '11':
$hub_rider_slots[3]--;
break;
case '12':
$hub_rider_slots[4]--;
break;
case '13':
$hub_rider_slots[5]--;
break;
case '14':
$hub_rider_slots[6]--;
break;
case '15':
$hub_rider_slots[7]--;
break;
default:
error_log('Does not fit in any time slot.' . $time_schedule);
}
}
}
// find first free slot
foreach ($hub_rider_slots as $index => $rider_slot)
{
if ($rider_slot > 0)
{
$flag_found_slot = true;
$jo_date_schedule = $start_date;
switch ($index) {
case '0':
$jo_date_schedule->setTime(8,0);
break;
case '1':
$jo_date_schedule->setTime(9,0);
break;
case '2':
$jo_date_schedule->setTime(10,0);
break;
case '3':
$jo_date_schedule->setTime(11,0);
break;
case '4':
$jo_date_schedule->setTime(12,0);
break;
case '5':
$jo_date_schedule->setTime(13,0);
break;
case '6':
$jo_date_schedule->setTime(14,0);
break;
case '7':
$jo_date_schedule->setTime(15,0);
break;
default:
error_log('Cannot find time slot.');
}
$results = [
'slot_found' => $flag_found_slot,
'date_schedule' => $jo_date_schedule,
];
break;
}
}
return $results;
}
}

View file

@ -152,7 +152,8 @@ class HubController extends Controller
->setTimeClose($time_close)
->setCoordinates($point)
->setBranchCode($req->request->get('branch_code', ''))
->setStatusOpen($req->request->get('status_open') ?? false);
->setStatusOpen($req->request->get('status_open') ?? false)
->setRiderSlots($req->request->get('rider_slots', 0));
}
protected function setQueryFilters($datatable, QueryBuilder $query)

View file

@ -56,6 +56,12 @@ class Hub
*/
protected $status_open;
// number of rider slots per day
/**
* @ORM\Column(type="integer")
*/
protected $rider_slots;
public function __construct()
{
$this->time_open = new DateTime();
@ -150,4 +156,15 @@ class Hub
return $this->status_open;
}
public function setRiderSlots($rider_slots)
{
$this->rider_slots = $rider_slots;
return $this;
}
public function getRiderSlots()
{
return $this->rider_slots;
}
}

View file

@ -90,7 +90,14 @@
<input type="text" name="branch_code" class="form-control m-input" value="{{ obj.getBranchCode() }}">
<div class="form-control-feedback hide" data-field="branch_code"></div>
</div>
<div class="col-lg-6">
<div class="col-lg-3">
<label for="rider_slots" data-field="rider_slots">
Number of Riders Per Slot
</label>
<input type="text" name="rider_slots" class="form-control m-input" value="{{ obj.getRiderSlots() }}">
<div class="form-control-feedback hide" data-field="rider_slots"></div>
</div>
<div class="col-lg-3">
<span class="m-switch m-switch--icon block-switch">
<label>
<input type="checkbox" name="status_open" id="status-open" value="1"{{ obj.isStatusOpen ? ' checked' }}>