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(); // get parent associations $params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll(); $params['customers'] = $em->getRepository(Customer::class)->findAll(); $params['promos'] = $em->getRepository(Promo::class)->findAll(); $params['service_types'] = ServiceType::getCollection(); $params['warranty_classes'] = WarrantyClass::getCollection(); $params['statuses'] = JOStatus::getCollection(); $params['discount_apply'] = DiscountApply::getCollection(); $params['trade_in_types'] = TradeInType::getCollection(); // response return $this->render('job-order/form.html.twig', $params); } public function incomingSubmit(Request $req, ValidatorInterface $validator, InvoiceCreator $ic) { error_log(print_r($req->request->all(), true)); $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')); // set and save values $obj->setDateCreate(DateTime::createFromFormat("d M Y", $req->request->get('date_transaction'))) ->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($req->request->get('service_type')) ->setWarrantyClass($req->request->get('warranty_class')) ->setCustomer($cust_vehicle->getCustomer()) ->setCustomerVehicle($cust_vehicle) ->setSource('web') ->setStatus(JOStatus::PENDING) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setAgentNotes($req->request->get('agent_notes')) ->setDeliveryAddress($req->request->get('delivery_address')); // instantiate invoice criteria $criteria = new InvoiceCriteria(); $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 = 'Processing'; $rows_route = 'jo_proc_rows'; $edit_route = 'jo_proc_form'; $jo_status = JOStatus::PENDING; break; case 'assign': $tier_key = 'jo_assign'; $tier_name = 'Assigning'; $rows_route = 'jo_assign_rows'; $edit_route = 'jo_assign_form'; $jo_status = JOStatus::RIDER_ASSIGN; 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, 'jo_status' => $jo_status ]; } public function listAssigning() { $params = $this->initParameters('jo_assign'); return $this->render('job-order/list.assigning.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']; // 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 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, $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, $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 $row['meta']['update_url'] = $this->generateUrl($tier_params['edit_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'; // get parent associations $params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll(); $params['customers'] = $em->getRepository(Customer::class)->findAll(); $params['service_types'] = ServiceType::getCollection(); $params['warranty_classes'] = WarrantyClass::getCollection(); $params['statuses'] = JOStatus::getCollection(); $params['promos'] = $em->getRepository(Promo::class)->findAll(); $params['discount_apply'] = DiscountApply::getCollection(); $params['trade_in_types'] = TradeInType::getCollection(); // get closest outlets $outlets = $map_tools->getClosestOutlets($obj->getCoordinates(), 10, date("H:i:s")); $params['outlets'] = []; // format duration and distance into friendly time foreach ($outlets as $outlet) { // duration $seconds = $outlet['duration']; if (!empty($seconds) && $seconds > 0) { $hours = floor($seconds / 3600); $minutes = ceil(($seconds / 60) % 60); $outlet['duration'] = ($hours > 0 ? number_format($hours) . " hr" . ($hours > 1 ? "s" : '') . ($minutes > 0 ? ", " : '') : '') . ($minutes > 0 ? number_format($minutes) . " min" . ($minutes > 1 ? "s" : '') : ''); } else { $outlet['duration'] = false; } // distance $meters = $outlet['distance']; if (!empty($meters) && $meters > 0) { $outlet['distance'] = round($meters / 1000) . " km"; } else { $outlet['distance'] = false; } $params['outlets'][] = $outlet; } $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 outlet is set if (empty($req->request->get('outlet'))) { $error_array['outlet'] = 'No outlet selected.'; } else { // get outlet $outlet = $em->getRepository(Outlet::class)->find($req->request->get('outlet')); if (empty($outlet)) { $error_array['outlet'] = 'Invalid outlet 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('web') ->setStatus(JOStatus::RIDER_ASSIGN) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setAgentNotes($req->request->get('agent_notes')) ->setDeliveryAddress($req->request->get('delivery_address')) ->setOutlet($outlet); // 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 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'); } // get parent associations $params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll(); $params['customers'] = $em->getRepository(Customer::class)->findAll(); $params['service_types'] = ServiceType::getCollection(); $params['warranty_classes'] = WarrantyClass::getCollection(); $params['statuses'] = JOStatus::getCollection(); $params['promos'] = $em->getRepository(Promo::class)->findAll(); $params['discount_apply'] = DiscountApply::getCollection(); $params['trade_in_types'] = TradeInType::getCollection(); // get closest outlets $outlets = $map_tools->getClosestOutlets($obj->getCoordinates(), 10, date("H:i:s")); $params['outlets'] = []; // format duration and distance into friendly time foreach ($outlets as $outlet) { // duration $seconds = $outlet['duration']; if (!empty($seconds) && $seconds > 0) { $hours = floor($seconds / 3600); $minutes = ceil(($seconds / 60) % 60); $outlet['duration'] = ($hours > 0 ? number_format($hours) . " hr" . ($hours > 1 ? "s" : '') . ($minutes > 0 ? ", " : '') : '') . ($minutes > 0 ? number_format($minutes) . " min" . ($minutes > 1 ? "s" : '') : ''); } else { $outlet['duration'] = false; } // distance $meters = $outlet['distance']; if (!empty($meters) && $meters > 0) { $outlet['distance'] = round($meters / 1000) . " km"; } else { $outlet['distance'] = false; } $params['outlets'][] = $outlet; } $params['obj'] = $obj; $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('web') ->setStatus(JOStatus::ASSIGNED) ->setDeliveryInstructions($req->request->get('delivery_instructions')) ->setAgentNotes($req->request->get('agent_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!' ]); } // 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, $status) { $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 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) { // 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 = false; $items = $req->request->get('items'); $promo_id = $req->request->get('promo'); $em = $this->getDoctrine()->getManager(); // instantiate invoice criteria $criteria = new InvoiceCriteria(); $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 ]); } }