denyAccessUnlessGranted('jo_in.list', null, 'No access.'); // get search term $term = $req->query->get('search'); // get querybuilder $qb = $this->getDoctrine() ->getRepository(JobOrder::class) ->createQueryBuilder('q'); // build expression now since we're reusing it $jo_label = $qb->expr()->concat($qb->expr()->literal('#'), 'q.id', $qb->expr()->literal(' - '), 'c.first_name', $qb->expr()->literal(' '), 'c.last_name', $qb->expr()->literal(' (Plate No: '), 'v.plate_number', $qb->expr()->literal(')')); // count total records $tquery = $qb->select('COUNT(q)') ->join('q.customer', 'c') ->join('q.cus_vehicle', 'v'); // add filters to count query if (!empty($term)) { $tquery->where($jo_label . ' LIKE :filter') ->setParameter('filter', '%' . $term . '%'); } $total = $tquery->getQuery() ->getSingleScalarResult(); // pagination vars $page = $req->query->get('page') ?? 1; $perpage = 20; $offset = ($page - 1) * $perpage; $pages = ceil($total / $perpage); $has_more_pages = $page < $pages ? true : false; // build main query $query = $qb->select('q') ->addSelect($jo_label . ' as jo_label') ->addSelect('c.first_name as cust_first_name') ->addSelect('c.last_name as cust_last_name') ->addSelect('v.plate_number as vehicle_plate_number'); // add filters if needed if (!empty($term)) { $query->where($jo_label . ' LIKE :filter') ->setParameter('filter', '%' . $term . '%'); } // get rows $obj_rows = $query->orderBy('q.id', 'asc') ->setFirstResult($offset) ->setMaxResults($perpage) ->getQuery() ->getResult(); // build job order array $job_orders = []; foreach ($obj_rows as $jo) { $service_type = ServiceType::getName($jo[0]->getServiceType()); $job_orders[] = [ 'id' => $jo[0]->getID(), 'text' => $jo['jo_label'] . ' - ' . $service_type ]; } // response return $this->json([ 'success' => true, 'results' => $job_orders, 'pagination' => [ 'more' => $has_more_pages ] ]); } protected function fillDropdownParameters(&$params) { $em = $this->getDoctrine()->getManager(); // db loaded $params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll(); $params['customers'] = $em->getRepository(Customer::class)->findAll(); $params['promos'] = $em->getRepository(Promo::class)->findAll(); // name values $params['service_types'] = ServiceType::getCollection(); $params['warranty_classes'] = WarrantyClass::getCollection(); $params['modes_of_payment'] = ModeOfPayment::getCollection(); $params['statuses'] = JOStatus::getCollection(); $params['discount_apply'] = DiscountApply::getCollection(); $params['trade_in_types'] = TradeInType::getCollection(); $params['sources'] = TransactionOrigin::getCollection(); } public function incomingForm() { $this->denyAccessUnlessGranted('jo_in.list', null, 'No access.'); $params = $this->initParameters('jo_in'); $params['obj'] = new JobOrder(); $params['mode'] = 'create'; $params['submit_url'] = $this->generateUrl('jo_in_submit'); $params['return_url'] = $this->generateUrl('jo_in'); $em = $this->getDoctrine()->getManager(); $this->fillDropdownParameters($params); // response return $this->render('job-order/form.html.twig', $params); } public function incomingSubmit(Request $req, ValidatorInterface $validator, InvoiceCreator $ic) { $this->denyAccessUnlessGranted('jo_in.list', null, 'No access.'); // initialize error list $error_array = []; // create new row $em = $this->getDoctrine()->getManager(); $obj = new JobOrder(); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } // 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.'; } } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); $stype = $req->request->get('service_type'); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setCreatedBy($this->getUser()) ->setServiceType($stype) ->setWarrantyClass($req->request->get('warranty_class')) ->setCustomer($cust_vehicle->getCustomer()) ->setCustomerVehicle($cust_vehicle) ->setSource($req->request->get('source')) ->setStatus(JOStatus::PENDING) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setORName($req->request->get('or_name')) ->setLandmark($req->request->get('landmark')); // 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 { $obj->setReferenceJO($ref_jo); } } // instantiate invoice criteria $criteria = new InvoiceCriteria(); $criteria->setServiceType($stype); $ierror = $this->invoicePromo($em, $criteria, $req->request->get('invoice_promo')); $invoice_items = $req->request->get('invoice_items'); if (!$ierror && !empty($invoice_items)) $ierror = $this->invoiceBatteries($em, $criteria, $invoice_items); if ($ierror) { $error_array['invoice'] = $ierror; } else { // generate the invoice $iobj = $ic->processCriteria($criteria); $iobj->setStatus(InvoiceStatus::DRAFT) ->setCreatedBy($this->getUser()); // validate $ierrors = $validator->validate($iobj); // add errors to list foreach ($ierrors as $error) { $error_array[$error->getPropertyPath()] = $error->getMessage(); } // add invoice to JO $obj->setInvoice($iobj); // save $em->persist($iobj); } // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->persist($obj); $em->flush(); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } protected function checkTier($tier) { // check specified tier switch ($tier) { case 'proc': $tier_key = 'jo_proc'; $tier_name = 'Dispatch'; $rows_route = 'jo_proc_rows'; $edit_route = 'jo_proc_form'; $unlock_route = 'jo_proc_unlock'; $jo_status = JOStatus::PENDING; break; case 'assign': $tier_key = 'jo_assign'; $tier_name = 'Assigning'; $rows_route = 'jo_assign_rows'; $edit_route = 'jo_assign_form'; $unlock_route = 'jo_assign_unlock'; $jo_status = JOStatus::RIDER_ASSIGN; break; case 'fulfill': $tier_key = 'jo_fulfill'; $tier_name = 'Fullfillment'; $rows_route = 'jo_fulfill_rows'; $edit_route = 'jo_fulfill_form'; $unlock_route = ''; $jo_status = [ JOStatus::ASSIGNED, JOStatus::IN_PROGRESS ]; break; case 'open': $tier_key = 'jo_open'; $tier_name = 'Open'; $rows_route = 'jo_open_rows'; $edit_route = ''; $unlock_route = ''; $jo_status = [ JOStatus::PENDING, JOStatus::RIDER_ASSIGN, JOStatus::ASSIGNED, JOStatus::IN_PROGRESS ]; break; case 'all': $tier_key = 'jo_open'; $tier_name = 'Open'; $rows_route = 'jo_open_rows'; $edit_route = 'jo_all_form'; $unlock_route = ''; $jo_status = ''; break; default: $exception = $this->createAccessDeniedException('No access.'); throw $exception; } // check acl $this->denyAccessUnlessGranted($tier_key . '.list', null, 'No access.'); // return params if allowed access return [ 'key' => $tier_key, 'name' => $tier_name, 'rows_route' => $rows_route, 'edit_route' => $edit_route, 'unlock_route' => $unlock_route, 'jo_status' => $jo_status ]; } public function listAssigning() { $this->denyAccessUnlessGranted('jo_assign.list', null, 'No access.'); $params = $this->initParameters('jo_assign'); $params['table_refresh_rate'] = $this->container->getParameter('job_order_refresh_interval'); return $this->render('job-order/list.assigning.html.twig', $params); } public function listFulfillment() { $this->denyAccessUnlessGranted('jo_fulfill.list', null, 'No access.'); $params = $this->initParameters('jo_fulfill'); $params['table_refresh_rate'] = $this->container->getParameter('job_order_refresh_interval'); return $this->render('job-order/list.fulfillment.html.twig', $params); } public function listOpen() { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); $params = $this->initParameters('jo_open'); $params['table_refresh_rate'] = $this->container->getParameter('job_order_refresh_interval'); $params['statuses'] = JOStatus::getCollection(); return $this->render('job-order/list.open.html.twig', $params); } public function listAll() { $this->denyAccessUnlessGranted('jo_all.list', null, 'No access.'); $params = $this->initParameters('jo_all'); $params['table_refresh_rate'] = $this->container->getParameter('job_order_refresh_interval'); return $this->render('job-order/list.all.html.twig', $params); } public function listRows($tier) { // check which job order tier is being called for and confirm access $tier_params = $this->checkTier($tier); $params = $this->initParameters($tier_params['key']); $params['tier_name'] = $tier_params['name']; $params['rows_route'] = $tier_params['rows_route']; $params['table_refresh_rate'] = $this->container->getParameter('job_order_refresh_interval'); // response return $this->render('job-order/list.html.twig', $params); } public function getRows(Request $req, $tier) { // check which job order tier is being called for and confirm access $tier_params = $this->checkTier($tier); // get current user $user = $this->getUser(); $hubs = $user->getHubs(); // get query builder $qb = $this->getDoctrine() ->getRepository(JobOrder::class) ->createQueryBuilder('q'); // get datatable params $datatable = $req->request->get('datatable'); // count total records $tquery = $qb->select('COUNT(q)'); $this->setQueryFilters($datatable, $tquery, $qb, $hubs, $tier, $tier_params['jo_status']); $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'); $this->setQueryFilters($datatable, $query, $qb, $hubs, $tier, $tier_params['jo_status']); // 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.date_schedule', 'asc'); } // get rows for this page $obj_rows = $query->setFirstResult($offset) ->setMaxResults($perpage) ->getQuery() ->getResult(); $statuses = JOStatus::getCollection(); $service_types = ServiceType::getCollection(); // process rows $rows = []; foreach ($obj_rows as $orow) { // add row data $row['id'] = $orow->getID(); $row['delivery_address'] = $orow->getDeliveryAddress(); $row['date_schedule'] = $orow->isAdvanceOrder() ? $orow->getDateSchedule()->format("d M Y g:i A") : 'Immediate'; $row['service_type'] = $service_types[$orow->getServiceType()]; $row['status'] = $statuses[$orow->getStatus()]; $row['flag_advance'] = $orow->isAdvanceOrder(); $processor = $orow->getProcessedBy(); if ($processor == null) $row['processor'] = ''; else $row['processor'] = $orow->getProcessedBy()->getFullName(); $assignor = $orow->getAssignedBy(); if ($assignor == null) $row['assignor'] = ''; else $row['assignor'] = $orow->getAssignedBy()->getFullName(); // add crud urls if ($tier == 'open') { $row['meta']['reassign_hub_url'] = $this->generateUrl('jo_open_hub_form', ['id' => $row['id']]); $row['meta']['reassign_rider_url'] = $this->generateUrl('jo_open_rider_form', ['id' => $row['id']]); } else { $row['meta']['update_url'] = $this->generateUrl($tier_params['edit_route'], ['id' => $row['id']]); } if ($tier_params['unlock_route'] != '') $row['meta']['unlock_url'] = $this->generateUrl($tier_params['unlock_route'], ['id' => $row['id']]); $rows[] = $row; } // response return $this->json([ 'meta' => $meta, 'data' => $rows ]); } public function processingForm(MapTools $map_tools, $id) { $this->denyAccessUnlessGranted('jo_proc.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); // manual transaction since we're locking $em->getConnection()->beginTransaction(); try { // lock and get data $obj = $em->getRepository(JobOrder::class)->find($id, LockMode::PESSIMISTIC_READ); // make sure this job order exists if (empty($obj)) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not exist'); } // check status if ($obj->getStatus() != JOStatus::PENDING) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not have a pending status'); } // check if we are the processor $processor = $obj->getProcessedBy(); $user = $this->getUser(); // TODO: go back to list page and display alert / flash that says they cannot access it because they // are not the processor if ($processor != null && $processor->getID() != $user->getID()) { $em->getConnection()->rollback(); throw $this->createAccessDeniedException('Not the processor'); } // make this user be the processor $obj->setProcessedBy($user); $em->flush(); $em->getConnection()->commit(); } catch(PessimisticLockException $e) { throw $this->createAccessDeniedException('Not the processor'); } // NOTE: we are able to lock, everything should be fine now $params = $this->initParameters('jo_proc'); $params['mode'] = 'update-processing'; $params['status_cancelled'] = JOStatus::CANCELLED; $this->fillDropdownParameters($params); // get closest hubs $hubs = $map_tools->getClosestHubs($obj->getCoordinates(), 10, date("H:i:s")); $params['hubs'] = []; // format duration and distance into friendly time foreach ($hubs as $hub) { // duration $seconds = $hub['duration']; if (!empty($seconds) && $seconds > 0) { $hours = floor($seconds / 3600); $minutes = ceil(($seconds / 60) % 60); $hub['duration'] = ($hours > 0 ? number_format($hours) . " hr" . ($hours > 1 ? "s" : '') . ($minutes > 0 ? ", " : '') : '') . ($minutes > 0 ? number_format($minutes) . " min" . ($minutes > 1 ? "s" : '') : ''); } else { $hub['duration'] = false; } // distance $meters = $hub['distance']; if (!empty($meters) && $meters > 0) { $hub['distance'] = round($meters / 1000) . " km"; } else { $hub['distance'] = false; } $params['hubs'][] = $hub; } $params['obj'] = $obj; $params['submit_url'] = $this->generateUrl('jo_proc_submit', ['id' => $obj->getID()]); $params['return_url'] = $this->generateUrl('jo_proc'); // response return $this->render('job-order/form.html.twig', $params); } public function processingSubmit(Request $req, ValidatorInterface $validator, $id) { $this->denyAccessUnlessGranted('jo_proc.list', null, 'No access.'); // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); $processor = $obj->getProcessedBy(); $user = $this->getUser(); // check if we're the one processing, return error otherwise if ($processor == null) throw $this->createAccessDeniedException('Not the processor'); if ($processor != null && $processor->getID() != $user->getID()) throw $this->createAccessDeniedException('Not the processor'); // initialize error list $error_array = []; // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } // check if hub is set if (empty($req->request->get('hub'))) { $error_array['hub'] = 'No hub selected.'; } else { // get hub $hub = $em->getRepository(Hub::class)->find($req->request->get('hub')); if (empty($hub)) { $error_array['hub'] = 'Invalid hub specified.'; } } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setServiceType($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setSource($req->request->get('source')) ->setStatus(JOStatus::RIDER_ASSIGN) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setHub($hub); // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->flush(); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } public function assigningForm(MapTools $map_tools, $id) { $this->denyAccessUnlessGranted('jo_assign.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); // manual transaction since we're locking $em->getConnection()->beginTransaction(); $params = $this->initParameters('jo_assign'); $params['mode'] = 'update-assigning'; try { // get row data $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this row exists if (empty($obj)) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not exist'); } // check status if ($obj->getStatus() != JOStatus::RIDER_ASSIGN) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not have an assigning status'); } // check if hub is assigned to current user $user_hubs = $this->getUser()->getHubs(); if (!in_array($obj->getHub()->getID(), $user_hubs)) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order is not on a hub assigned to this user'); } // check if we are the assignor $assignor = $obj->getAssignedBy(); $user = $this->getUser(); if ($assignor != null && $assignor->getID() != $user->getID()) { $em->getConnection()->rollback(); throw $this->createAccessDeniedException('Not the assignor'); } // make this user be the assignor $obj->setAssignedBy($user); $em->flush(); $em->getConnection()->commit(); } catch (PessimisticLockException $e) { throw $this->createAccessDeniedException('Not the assignor'); } $this->fillDropdownParameters($params); $params['obj'] = $obj; $params['status_cancelled'] = JOStatus::CANCELLED; $params['submit_url'] = $this->generateUrl('jo_assign_submit', ['id' => $obj->getID()]); $params['return_url'] = $this->generateUrl('jo_assign'); // response return $this->render('job-order/form.html.twig', $params); } public function assigningSubmit(Request $req, ValidatorInterface $validator, $id) { $this->denyAccessUnlessGranted('jo_assign.list', null, 'No access.'); // initialize error list $error_array = []; // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } // check if rider is set if (empty($req->request->get('rider'))) { $error_array['rider'] = 'No rider selected.'; } else { // get rider $rider = $em->getRepository(Rider::class)->find($req->request->get('rider')); if (empty($rider)) { $error_array['rider'] = 'Invalid rider specified.'; } } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setServiceType($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setSource($req->request->get('source')) ->setStatus(JOStatus::ASSIGNED) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setAssignedBy($this->getUser()) ->setDateAssign(new DateTime()) ->setRider($rider); // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->flush(); // send event to mobile app $payload = [ 'event' => 'driver_assigned' ]; $this->sendEvent($obj, $payload); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } public function fulfillmentForm(MapTools $map_tools, $id) { $this->denyAccessUnlessGranted('jo_fulfill.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); $params = $this->initParameters('jo_fulfill'); $params['mode'] = 'update-fulfillment'; // get row data $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this row exists if (empty($obj)) { throw $this->createNotFoundException('The job order does not exist'); } // check status if (!in_array($obj->getStatus(), [JOStatus::ASSIGNED, JOStatus::IN_PROGRESS])) { throw $this->createNotFoundException('The job order does not have a fulfillment status'); } // check if hub is assigned to current user $user_hubs = $this->getUser()->getHubs(); if (!in_array($obj->getHub()->getID(), $user_hubs)) { throw $this->createNotFoundException('The job order is not on a hub assigned to this user'); } $this->fillDropdownParameters($params); $params['obj'] = $obj; $params['status_cancelled'] = JOStatus::CANCELLED; $params['submit_url'] = $this->generateUrl('jo_fulfill_submit', ['id' => $obj->getID()]); $params['return_url'] = $this->generateUrl('jo_fulfill'); // response return $this->render('job-order/form.html.twig', $params); } public function fulfillmentSubmit(Request $req, ValidatorInterface $validator, $id) { $this->denyAccessUnlessGranted('jo_fulfill.list', null, 'No access.'); // initialize error list $error_array = []; // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setServiceType($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setSource($req->request->get('source')) ->setStatus(JOStatus::FULFILLED) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')); // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->flush(); // get rider $rider = $obj->getRider(); $image_url = $req->getScheme() . '://' . $req->getHttpHost() . $req->getBasePath() . '/assets/images/user.gif'; if ($rider->getImageFile() != null) $image_url = $req->getScheme() . '://' . $req->getHttpHost() . $req->getBasePath() . '/uploads/' . $rider->getImageFile(); // send to mqtt $payload = [ 'event' => 'fulfilled', 'jo_id' => $obj->getID(), 'driver_image' => $image_url, 'driver_name' => $rider->getFullName(), 'driver_id' => $rider->getID(), ]; $this->sendEvent($obj, $payload); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } protected function sendEvent(JobOrder $job_order, $payload) { $session = $job_order->getCustomer()->getMobileSessions(); if (count($session) == 0) return; $phone_num = $session[0]->getPhoneNumber(); $channel = 'motolite.control.' . $phone_num; $client = new MosquittoClient(); $client->connect('localhost', 1883); $client->publish($channel, json_encode($payload)); $client->disconnect(); } public function openHubForm(MapTools $map_tools, $id) { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); $params = $this->initParameters('jo_open'); $params['mode'] = 'update-reassign-hub'; // get row data $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this row exists if (empty($obj)) { throw $this->createNotFoundException('The job order does not exist'); } $this->fillDropdownParameters($params); // get closest hubs $hubs = $map_tools->getClosestHubs($obj->getCoordinates(), 10, date("H:i:s")); $params['status_cancelled'] = JOStatus::CANCELLED; $params['hubs'] = []; // format duration and distance into friendly time foreach ($hubs as $hub) { // duration $seconds = $hub['duration']; if (!empty($seconds) && $seconds > 0) { $hours = floor($seconds / 3600); $minutes = ceil(($seconds / 60) % 60); $hub['duration'] = ($hours > 0 ? number_format($hours) . " hr" . ($hours > 1 ? "s" : '') . ($minutes > 0 ? ", " : '') : '') . ($minutes > 0 ? number_format($minutes) . " min" . ($minutes > 1 ? "s" : '') : ''); } else { $hub['duration'] = false; } // distance $meters = $hub['distance']; if (!empty($meters) && $meters > 0) { $hub['distance'] = round($meters / 1000) . " km"; } else { $hub['distance'] = false; } $params['hubs'][] = $hub; } $params['obj'] = $obj; $params['submit_url'] = $this->generateUrl('jo_open_hub_submit', ['id' => $obj->getID()]); $params['return_url'] = $this->generateUrl('jo_open'); // response return $this->render('job-order/form.html.twig', $params); } public function openHubSubmit(Request $req, ValidatorInterface $validator, $id) { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); $user = $this->getUser(); // initialize error list $error_array = []; // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } // check if hub is set if (empty($req->request->get('hub'))) { $error_array['hub'] = 'No hub selected.'; } else { // get hub $hub = $em->getRepository(Hub::class)->find($req->request->get('hub')); if (empty($hub)) { $error_array['hub'] = 'Invalid hub specified.'; } } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setServiceType($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setSource($req->request->get('source')) ->setStatus(JOStatus::RIDER_ASSIGN) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setHub($hub) ->clearRider(); // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->flush(); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } public function openRiderForm($id) { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); $params = $this->initParameters('jo_open'); $params['mode'] = 'update-reassign-rider'; // get row data $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this row exists if (empty($obj)) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not exist'); } // check status if ($obj->getStatus() == JOStatus::PENDING) { $em->getConnection()->rollback(); throw $this->createNotFoundException('The job order does not have an assigned hub'); } $this->fillDropdownParameters($params); $params['obj'] = $obj; $params['status_cancelled'] = JOStatus::CANCELLED; $params['submit_url'] = $this->generateUrl('jo_open_rider_submit', ['id' => $obj->getID()]); $params['return_url'] = $this->generateUrl('jo_open'); // response return $this->render('job-order/form.html.twig', $params); } public function openRiderSubmit(Request $req, ValidatorInterface $validator, $id) { $this->denyAccessUnlessGranted('jo_open.list', null, 'No access.'); // initialize error list $error_array = []; // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // check if lat and lng are provided if (empty($req->request->get('coord_lng')) || empty($req->request->get('coord_lat'))) { $error_array['coordinates'] = 'No map coordinates provided. Please click on a location on the map.'; } // check if rider is set if (empty($req->request->get('rider'))) { $error_array['rider'] = 'No rider selected.'; } else { // get rider $rider = $em->getRepository(Rider::class)->find($req->request->get('rider')); if (empty($rider)) { $error_array['rider'] = 'Invalid rider specified.'; } } if (empty($error_array)) { // coordinates $point = new Point($req->request->get('coord_lng'), $req->request->get('coord_lat')); // set and save values $obj->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) ->setCoordinates($point) ->setAdvanceOrder($req->request->get('flag_advance') ?? false) ->setServiceType($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setSource($req->request->get('source')) ->setStatus(JOStatus::ASSIGNED) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setTier1Notes($req->request->get('tier1_notes')) ->setTier2Notes($req->request->get('tier2_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setAssignedBy($this->getUser()) ->setDateAssign(new DateTime()) ->setRider($rider); // validate $errors = $validator->validate($obj); // 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); } // validated! save the entity $em->flush(); // return successful response return $this->json([ 'success' => 'Changes have been saved!' ]); } public function allForm($id) { $this->denyAccessUnlessGranted('jo_all.list', null, 'No access.'); $em = $this->getDoctrine()->getManager(); $params = $this->initParameters('jo_all'); $params['mode'] = 'update-all'; // get row data $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this row exists if (empty($obj)) throw $this->createNotFoundException('The job order does not exist'); $this->fillDropdownParameters($params); $params['obj'] = $obj; $params['status_cancelled'] = JOStatus::CANCELLED; $params['return_url'] = $this->generateUrl('jo_all'); $params['submit_url'] = ''; // timeline stuff (descending by time) $params['timeline'] = [ [ 'date' => date("M j"), 'time' => date("g:i A"), 'event' => "Event 4", 'color' => "#f4516c" ], [ 'date' => date("M j"), 'time' => date("g:i A"), 'event' => "Event 3", 'color' => "#34bfa3" ], [ 'date' => date("M j"), 'time' => date("g:i A"), 'event' => "Event 2", 'color' => "#716aca" ], [ 'date' => date("M j"), 'time' => date("g:i A"), 'event' => "Event 1", 'color' => "#ffb822" ], ]; // response return $this->render('job-order/form.html.twig', $params); } public function cancelJobOrder(Request $req, $id) { $this->denyAccessUnlessGranted('joborder.cancel', null, 'No access.'); $cancel_reason = $req->request->get('cancel_reason'); if (empty($cancel_reason)) { // something happened return $this->json([ 'success' => false, 'error' => 'Reason for cancellation is required.' ], 422); } // get object data $em = $this->getDoctrine()->getManager(); $obj = $em->getRepository(JobOrder::class)->find($id); // make sure this object exists if (empty($obj)) throw $this->createNotFoundException('The item does not exist'); // cancel job order $obj->setStatus(JOStatus::CANCELLED) ->setCancelReason($cancel_reason); // save $em->flush(); // return successful response return $this->json([ 'success' => 'Job order has been cancelled!' ]); } // TODO: re-enable search, figure out how to group the orWhere filters into one, so can execute that plus the pending filter // check if datatable filter is present and append to query protected function setQueryFilters($datatable, &$query, $qb, $hubs, $tier, $status) { switch ($tier) { case 'fulfill': $query->where('q.status IN (:statuses)') ->andWhere('q.hub IN (:hubs)') ->setParameter('statuses', $status, Connection::PARAM_STR_ARRAY) ->setParameter('hubs', $hubs, Connection::PARAM_STR_ARRAY); break; case 'assign': $query->where('q.status = :status') ->andWhere('q.hub IN (:hubs)') ->setParameter('status', $status) ->setParameter('hubs', $hubs, Connection::PARAM_STR_ARRAY); break; case 'open': $query->where('q.status IN (:statuses)') ->setParameter('statuses', $status, Connection::PARAM_STR_ARRAY); break; case 'all': break; default: $query->where('q.status = :status') ->setParameter('status', $status); } // get only pending rows /* $query->where($qb->expr()->orX( $qb->expr()where('q.status', 'pending'); )); // apply filters if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) { $query->where('q.delivery_address LIKE :filter') ->orWhere($qb->expr()->concat('c.first_name', $qb->expr()->literal(' '), 'c.last_name') . ' LIKE :filter') ->orWhere('cv.plate_number LIKE :filter') ->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%'); } */ } protected function invoicePromo($em, InvoiceCriteria $criteria, $promo_id) { // return error if there's a problem, false otherwise // check service type $stype = $criteria->getServiceType(); if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW) return null; if (empty($promo_id)) return false; // check if this is a valid promo $promo = $em->getRepository(Promo::class)->find($promo_id); if (empty($promo)) return 'Invalid promo specified.'; $criteria->addPromo($promo); return false; } protected function invoiceBatteries($em, InvoiceCriteria $criteria, $items) { // check service type $stype = $criteria->getServiceType(); if ($stype != ServiceType::BATTERY_REPLACEMENT_NEW && $stype != ServiceType::BATTERY_REPLACEMENT_WARRANTY) return null; // return error if there's a problem, false otherwise if (!empty($items)) { foreach ($items as $item) { // check if this is a valid battery $battery = $em->getRepository(Battery::class)->find($item['battery']); if (empty($battery)) { $error = 'Invalid battery specified.'; return $error; } // quantity $qty = $item['quantity']; if ($qty < 1) continue; // add to criteria $criteria->addBattery($battery, $qty); // if this is a trade in, add trade in if (!empty($item['trade_in']) && TradeInType::validate($item['trade_in'])) $criteria->addTradeIn($item['trade_in'] == 'motolite', $qty); } } return null; } public function generateInvoice(Request $req, InvoiceCreator $ic) { error_log('generating invoice...'); $error = false; $stype = $req->request->get('stype'); $items = $req->request->get('items'); $promo_id = $req->request->get('promo'); // instantiate invoice criteria $criteria = new InvoiceCriteria(); $criteria->setServiceType($stype); $em = $this->getDoctrine()->getManager(); /* // if it's a jumpstart or troubleshoot only, we know what to charge already if ($stype == ServiceType::JUMPSTART_TROUBLESHOOT) { $invoice = [ 'price' => 150.00, 'discount' => 0, 'trade_in' => 0, 'vat' => 0, 'total_price' => 150.00, 'items' => [] ]; $invoice['items'][] = [ 'title' => 'Troubleshooting fee', 'quantity' => 1, 'unit_price' => 150.00, 'amount' => 150.00 ]; return $this->json([ 'success' => true, 'invoice' => $invoice ]); } */ $error = $this->invoicePromo($em, $criteria, $promo_id); if (!$error) $error = $this->invoiceBatteries($em, $criteria, $items); if ($error) { // something happened return $this->json([ 'success' => false, 'error' => $error ], 422); } // generate the invoice $iobj = $ic->processCriteria($criteria); // use invoice object values in a json friendly array $invoice = [ 'discount' => number_format($iobj->getDiscount(), 2), 'trade_in' => number_format($iobj->getTradeIn(), 2), // TODO: computations not done yet for this on invoice creator 'price' => number_format($iobj->getVATExclusivePrice(), 2), 'vat' => number_format($iobj->getVAT(), 2), 'total_price' => number_format($iobj->getTotalPrice(), 2), 'items' => [] ]; foreach ($iobj->getItems() as $item) { $invoice['items'][] = [ 'title' => $item->getTitle(), 'quantity' => number_format($item->getQuantity()), // TODO: quantities are always 1, hardcoded into InvoiceCreator. no way of accepting quantities on InvoiceCriteria 'unit_price' => number_format($item->getPrice(), 2), 'amount' => number_format($item->getPrice() * $item->getQuantity(), 2) // TODO: should this calculation should be a saved value on InvoiceItem instead? ]; } // return return $this->json([ 'success' => true, 'invoice' => $invoice ]); } public function unlockProcessor($id) { $this->denyAccessUnlessGranted('jo_proc.unlock', null, 'No access.'); // clear lock $em = $this->getDoctrine()->getManager(); $jo = $em->getRepository(JobOrder::class)->find($id); if ($jo == null) return $this->redirectToRoute('jo_proc'); $jo->setProcessedBy(null); $em->flush(); // redirect to list return $this->redirectToRoute('jo_proc'); } public function unlockAssignor($id) { $this->denyAccessUnlessGranted('jo_assign.unlock', null, 'No access.'); // clear lock $em = $this->getDoctrine()->getManager(); $jo = $em->getRepository(JobOrder::class)->find($id); if ($jo == null) return $this->redirectToRoute('jo_assign'); $jo->setAssignedBy(null); $em->flush(); // redirect to list return $this->redirectToRoute('jo_assign'); } }