diff --git a/config/acl.yaml b/config/acl.yaml index 90b2f06d..c6ccc243 100644 --- a/config/acl.yaml +++ b/config/acl.yaml @@ -215,6 +215,20 @@ access_keys: - id: rider.delete label: Delete + - id: servicecharge + label: Service Charge + acls: + - id: service_charge.menu + label: Menu + - id: service_charge.list + label: List + - id: service_charge.add + label: Add + - id: service_charge.update + label: Update + - id: service_charge.delete + label: Delete + - id: joborder label: Job Order acls: @@ -246,6 +260,10 @@ access_keys: label: One-step Process - id: jo_onestep.edit label: One-step Process Edit + - id: jo_walkin.form + label: Walk-in + - id: jo_walkin.edit + label: Walk-in Edit - id: support label: Customer Support Access diff --git a/config/cmb.menu.yaml b/config/cmb.menu.yaml new file mode 100644 index 00000000..7c6b4d82 --- /dev/null +++ b/config/cmb.menu.yaml @@ -0,0 +1,183 @@ +main_menu: + - id: home + acl: dashboard.menu + label: Dashboard + icon: flaticon-line-graph + - id: user + acl: user.menu + label: User + icon: flaticon-users + - id: user_list + acl: user.list + label: Users + parent: user + - id: role_list + acl: role.list + label: Roles + parent: user + + - id: apiuser + acl: apiuser.menu + label: API User + icon: flaticon-users + - id: api_user_list + acl: apiuser.list + label: API Users + parent: apiuser + - id: api_role_list + acl: apirole.list + label: API Roles + parent: apiuser + + - id: logistics + acl: logistics.menu + label: Logistics + icon: fa fa-truck + - id: rider_list + acl: rider.list + label: Riders + parent: logistics + - id: service_charge_list + acl: service_charge.list + label: Service Charges + parent: logistics + + - id: battery + acl: battery.menu + label: Battery + icon: fa fa-battery-3 + - id: battery_list + acl: battery.list + label: Batteries + parent: battery + - id: bmfg_list + acl: bmfg.list + label: Manufacturers + parent: battery + - id: bmodel_list + acl: bmodel.list + label: Models + parent: battery + - id: bsize_list + acl: bsize.list + label: Sizes + parent: battery + - id: promo_list + acl: promo.list + label: Promos + parent: battery + + - id: vehicle + acl: vehicle.menu + label: Vehicle + icon: fa fa-car + - id: vehicle_list + acl: vehicle.list + label: Vehicles + parent: vehicle + - id: vmfg_list + acl: vmfg.list + label: Manufacturers + parent: vehicle + + - id: location + acl: location.menu + label: Location + icon: fa fa-home + - id: outlet_list + acl: outlet.menu + label: Outlet + parent: location + - id: hub_list + acl: hub.menu + label: Hub + parent: location + - id: geofence_list + acl: geofence.menu + label: Geofence + parent: location + + + - id: joborder + acl: joborder.menu + label: Job Order + icon: flaticon-calendar-3 + - id: jo_onestep_form + acl: jo_onestep.form + label: One-step Process + parent: joborder + - id: jo_walkin_form + acl: jo_walkin.form + label: Walk-in + parent: joborder + - id: jo_fulfill + acl: jo_fulfill.list + label: Fulfillment + parent: joborder + - id: jo_open + acl: jo_open.list + label: Open + parent: joborder + - id: jo_all + acl: jo_all.list + label: View All + parent: joborder + + - id: support + acl: support.menu + label: Customer Support + icon: flaticon-support + - id: customer_list + acl: customer.list + label: Customers + parent: support + - id: ticket_list + acl: ticket.list + label: Tickets + parent: support + - id: general_search + acl: general.search + label: Search + parent: support + - id: warranty_search + acl: warranty.search + label: Customer Battery Search + parent: support + - id: privacy_policy_list + acl: privacy_policy.list + label: Privacy Policy + parent: support + - id: warranty_list + acl: warranty.list + label: Warranty + parent: support + - id: warranty_upload + acl: warranty.upload + label: Warranty Upload + parent: support + - id: static_content_list + acl: static_content.list + label: Static Content + parent: support + + - id: service + acl: service.menu + label: Other Services + icon: flaticon-squares + - id: service_list + acl: service.list + label: Services + parent: service + + - id: partner + acl: partner.menu + label: Partners + icon: flaticon-network + - id: partner_list + acl: partner.list + label: Partners + parent: partner + - id: review_list + acl: review.list + label: Reviews + parent: partner diff --git a/config/cmb.services.yaml b/config/cmb.services.yaml index c069dc5f..d12b2acf 100644 --- a/config/cmb.services.yaml +++ b/config/cmb.services.yaml @@ -189,7 +189,7 @@ services: App\Service\RiderAPIHandler\CMBRiderAPIHandler: arguments: $country_code: "%env(COUNTRY_CODE)%" - + # rider API interface App\Service\RiderAPIHandlerInterface: "@App\\Service\\RiderAPIHandler\\CMBRiderAPIHandler" diff --git a/config/menu.yaml b/config/menu.yaml index a64dd8b4..7c6b4d82 100644 --- a/config/menu.yaml +++ b/config/menu.yaml @@ -37,6 +37,10 @@ main_menu: acl: rider.list label: Riders parent: logistics + - id: service_charge_list + acl: service_charge.list + label: Service Charges + parent: logistics - id: battery acl: battery.menu @@ -102,17 +106,9 @@ main_menu: acl: jo_onestep.form label: One-step Process parent: joborder - - id: jo_in - acl: jo_in.list - label: Incoming - parent: joborder - - id: jo_proc - acl: jo_proc.list - label: Dispatch - parent: joborder - - id: jo_assign - acl: jo_assign.list - label: Rider Assignment + - id: jo_walkin_form + acl: jo_walkin.form + label: Walk-in parent: joborder - id: jo_fulfill acl: jo_fulfill.list diff --git a/config/resq.menu.yaml b/config/resq.menu.yaml new file mode 100644 index 00000000..b0096cf5 --- /dev/null +++ b/config/resq.menu.yaml @@ -0,0 +1,183 @@ +main_menu: + - id: home + acl: dashboard.menu + label: Dashboard + icon: flaticon-line-graph + - id: user + acl: user.menu + label: User + icon: flaticon-users + - id: user_list + acl: user.list + label: Users + parent: user + - id: role_list + acl: role.list + label: Roles + parent: user + + - id: apiuser + acl: apiuser.menu + label: API User + icon: flaticon-users + - id: api_user_list + acl: apiuser.list + label: API Users + parent: apiuser + - id: api_role_list + acl: apirole.list + label: API Roles + parent: apiuser + + - id: logistics + acl: logistics.menu + label: Logistics + icon: fa fa-truck + - id: rider_list + acl: rider.list + label: Riders + parent: logistics + + - id: battery + acl: battery.menu + label: Battery + icon: fa fa-battery-3 + - id: battery_list + acl: battery.list + label: Batteries + parent: battery + - id: bmfg_list + acl: bmfg.list + label: Manufacturers + parent: battery + - id: bmodel_list + acl: bmodel.list + label: Models + parent: battery + - id: bsize_list + acl: bsize.list + label: Sizes + parent: battery + - id: promo_list + acl: promo.list + label: Promos + parent: battery + + - id: vehicle + acl: vehicle.menu + label: Vehicle + icon: fa fa-car + - id: vehicle_list + acl: vehicle.list + label: Vehicles + parent: vehicle + - id: vmfg_list + acl: vmfg.list + label: Manufacturers + parent: vehicle + + - id: location + acl: location.menu + label: Location + icon: fa fa-home + - id: outlet_list + acl: outlet.menu + label: Outlet + parent: location + - id: hub_list + acl: hub.menu + label: Hub + parent: location + - id: geofence_list + acl: geofence.menu + label: Geofence + parent: location + + + - id: joborder + acl: joborder.menu + label: Job Order + icon: flaticon-calendar-3 + - id: jo_in + acl: jo_in.list + label: Incoming + parent: joborder + - id: jo_proc + acl: jo_proc.list + label: Dispatch + parent: joborder + - id: jo_assign + acl: jo_assign.list + label: Rider Assignment + parent: joborder + - id: jo_fulfill + acl: jo_fulfill.list + label: Fulfillment + parent: joborder + - id: jo_open + acl: jo_open.list + label: Open + parent: joborder + - id: jo_all + acl: jo_all.list + label: View All + parent: joborder + + - id: support + acl: support.menu + label: Customer Support + icon: flaticon-support + - id: customer_list + acl: customer.list + label: Customers + parent: support + - id: ticket_list + acl: ticket.list + label: Tickets + parent: support + - id: general_search + acl: general.search + label: Search + parent: support + - id: warranty_search + acl: warranty.search + label: Customer Battery Search + parent: support + - id: privacy_policy_list + acl: privacy_policy.list + label: Privacy Policy + parent: support + - id: warranty_list + acl: warranty.list + label: Warranty + parent: support + - id: warranty_upload + acl: warranty.upload + label: Warranty Upload + parent: support + - id: static_content_list + acl: static_content.list + label: Static Content + parent: support + + - id: service + acl: service.menu + label: Other Services + icon: flaticon-squares + - id: service_list + acl: service.list + label: Services + parent: service + + - id: partner + acl: partner.menu + label: Partners + icon: flaticon-network + - id: partner_list + acl: partner.list + label: Partners + parent: partner + - id: review_list + acl: review.list + label: Reviews + parent: partner diff --git a/config/routes/job_order.yaml b/config/routes/job_order.yaml index bfd41df9..d5cd8dfc 100644 --- a/config/routes/job_order.yaml +++ b/config/routes/job_order.yaml @@ -206,3 +206,23 @@ jo_tracker: controller: App\Controller\JobOrderController::tracker methods: [GET] +jo_walkin_form: + path: /job-order/walk-in + controller: App\Controller\JobOrderController::walkInForm + methods: [GET] + +jo_walkin_submit: + path: /job-order/walk-in + controller: App\Controller\JobOrderController::walkInSubmit + methods: [POST] + +jo_walkin_edit_form: + path: /job-order/walk-in/{id} + controller: App\Controller\JobOrderController::walkInEditForm + methods: [GET] + +jo_walkin_edit_submit: + path: /job-order/walk-in/{id} + controller: App\Controller\JobOrderController::walkInEditSubmit + methods: [POST] + diff --git a/config/routes/rider.yaml b/config/routes/rider.yaml index 70ddd91d..16a56993 100644 --- a/config/routes/rider.yaml +++ b/config/routes/rider.yaml @@ -41,3 +41,8 @@ rider_ajax_popup: path: /riders/{id}/popup controller: App\Controller\RiderController::popupInfo methods: [GET] + +rider_active_jo: + path: /riders/{id}/activejo/{jo_id} + controller: App\Controller\RiderController::riderActiveJO + methods: [GET] diff --git a/config/routes/service_charge.yaml b/config/routes/service_charge.yaml new file mode 100644 index 00000000..c6206371 --- /dev/null +++ b/config/routes/service_charge.yaml @@ -0,0 +1,34 @@ +service_charge_list: + path: /service_charges + controller: App\Controller\ServiceChargeController::index + +service_charge_rows: + path: /service_charges/rows + controller: App\Controller\ServiceChargeController::rows + methods: [POST] + +service_charge_create: + path: /service_charges/create + controller: App\Controller\ServiceChargeController::addForm + methods: [GET] + +service_charge_create_submit: + path: /service_charges/create + controller: App\Controller\ServiceChargeController::addSubmit + methods: [POST] + +service_charge_update: + path: /service_charges/{id} + controller: App\Controller\ServiceChargeController::updateForm + methods: [GET] + +service_charge_update_submit: + path: /service_charges/{id} + controller: App\Controller\ServiceChargeController::updateSubmit + methods: [POST] + +service_charge_delete: + path: /service_charges/{id} + controller: App\Controller\ServiceChargeController::destroy + methods: [DELETE] + diff --git a/initial_sql/sql_insert_service_charge_data.sql b/initial_sql/sql_insert_service_charge_data.sql new file mode 100644 index 00000000..bab3fdf5 --- /dev/null +++ b/initial_sql/sql_insert_service_charge_data.sql @@ -0,0 +1 @@ +INSERT INTO `service_charge` VALUES(1,'Bangi',20),(2,'Banting',30),(3,'Bdr Saujana Utama',20),(4,'Bdr Seri Coalfields',30),(5,'Bdr Baru Bangi',20),(6,'Bdr Saujana Putra',20),(7,'Bukit Beruntung',30),(8,'Cyberjaya',20),(9,'Dengkil',30),(10,'Hulu Langat',20),(11,'Jenjarom',30),(12,'Klia',30),(13,'Meru',20),(14,'Port Klang',20),(15,'Pulau Indah',30),(16,'Puncak Alam',20),(17,'Putrajaya',20),(18,'Rawang',30),(19,'Salak Tinggi',30),(20,'Semenyih',20),(21,'Sepang',30),(22,'Serendah',30),(23,'Sungai Buloh',20),(24,'Teluk Panglima Garang',30),(25,'Uitm Puncak Alam',20),(26,'12am - 7am',10),(27,'Out of define Klg Valley',20),(28,'Airport',35),(29,'Jump start',50),(30,'Product warranty service charge - existing BA customer',20),(31,'Product warranty service charge - non BA customer',40); diff --git a/src/Controller/JobOrderController.php b/src/Controller/JobOrderController.php index f70d5484..c06383c7 100644 --- a/src/Controller/JobOrderController.php +++ b/src/Controller/JobOrderController.php @@ -12,6 +12,7 @@ use App\Entity\Battery; use App\Entity\JobOrder; use App\Entity\VehicleManufacturer; use App\Entity\Vehicle; +use App\Entity\Hub; use App\Service\InvoiceGeneratorInterface; use App\Service\JobOrderHandlerInterface; @@ -604,7 +605,7 @@ class JobOrderController extends Controller * @Menu(selected="jo_all") */ public function allForm($id, JobOrderHandlerInterface $jo_handler, - GISManagerInterface $gis) + GISManagerInterface $gis, EntityManagerInterface $em) { $this->denyAccessUnlessGranted('jo_all.list', null, 'No access.'); @@ -617,6 +618,8 @@ class JobOrderController extends Controller throw $this->createNotFoundException($e->getMessage()); } + $params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll(); + $params['vmakes'] = $em->getRepository(Vehicle::class)->findAll(); $params['return_url'] = $this->generateUrl('jo_all'); $params['submit_url'] = ''; $params['map_js_file'] = $gis->getJSJOFile(); @@ -691,6 +694,7 @@ class JobOrderController extends Controller $items = $req->request->get('items'); $promo_id = $req->request->get('promo'); $cvid = $req->request->get('cvid'); + $service_charges = $req->request->get('service_charges'); $em = $this->getDoctrine()->getManager(); @@ -735,7 +739,10 @@ class JobOrderController extends Controller */ // TODO: this snippet should be in the invoice generator - $error = $ic->invoicePromo($criteria, $promo_id); + $error = $ic->validateDiscount($criteria, $promo_id); + + // process service charges + $error = $ic->invoiceServiceCharges($criteria, $service_charges); if (!$error) $error = $ic->invoiceBatteries($criteria, $items); @@ -880,7 +887,6 @@ class JobOrderController extends Controller return $this->json([ 'success' => 'Changes have been saved!' ]); - } /** @@ -962,4 +968,93 @@ class JobOrderController extends Controller return $this->render('job-order/tracker.html.twig', $params); } + + /** + * @Menu(selected="jo_walkin_form") + */ + public function walkInForm(EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler) + { + $this->denyAccessUnlessGranted('jo_walkin.form', null, 'No access.'); + + $params = $jo_handler->initializeWalkinForm(); + $params['submit_url'] = $this->generateUrl('jo_walkin_submit'); + $params['return_url'] = $this->generateUrl('jo_walkin_form'); + $params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll(); + $params['vmakes'] = $em->getRepository(Vehicle::class)->findAll(); + $params['hubs'] = $em->getRepository(Hub::class)->findAll(); + + $template = $params['template']; + + // response + return $this->render($template, $params); + } + + public function walkInSubmit(Request $req, JobOrderHandlerInterface $jo_handler) + { + $this->denyAccessUnlessGranted('jo_walkin.form', null, 'No access.'); + + // initialize error list + $error_array = []; + $id = -1; + $error_array = $jo_handler->processWalkinJobOrder($req, $id); + + // check if any errors were found + if (!empty($error_array)) { + // return validation failure response + return $this->json([ + 'success' => false, + 'errors' => $error_array + ], 422); + } + + // return successful response + return $this->json([ + 'success' => 'Changes have been saved!' + ]); + } + + /** + * @Menu(selected="jo_walkin_edit_form") + */ + public function walkInEditForm($id, EntityManagerInterface $em, JobOrderHandlerInterface $jo_handler) + { + $this->denyAccessUnlessGranted('jo_walkin.edit', null, 'No access.'); + + $params = $jo_handler->initializeWalkinEditForm($id); + $params['submit_url'] = $this->generateUrl('jo_walkin_edit_submit', ['id' => $id]); + $params['return_url'] = $this->generateUrl('jo_open'); + $params['vmfgs'] = $em->getRepository(VehicleManufacturer::class)->findAll(); + $params['vmakes'] = $em->getRepository(Vehicle::class)->findAll(); + $params['hubs'] = $em->getRepository(Hub::class)->findAll(); + + $template = $params['template']; + + // response + return $this->render($template, $params); + } + + public function walkInEditSubmit(Request $req, JobOrderHandlerInterface $jo_handler) + { + $this->denyAccessUnlessGranted('jo_walkin.edit', null, 'No access.'); + + $error_array = []; + $error_array = $jo_handler->processOneStepJobOrder($req, $id); + + // check if any errors were found + if (!empty($error_array)) { + // return validation failure response + return $this->json([ + 'success' => false, + 'errors' => $error_array + ], 422); + } + + + // return successful response + return $this->json([ + 'success' => 'Changes have been saved!' + ]); + } + + } diff --git a/src/Controller/RiderController.php b/src/Controller/RiderController.php index 838b558b..ace84582 100644 --- a/src/Controller/RiderController.php +++ b/src/Controller/RiderController.php @@ -7,7 +7,10 @@ use App\Entity\Rider; use App\Entity\RiderSchedule; use App\Entity\Hub; use App\Entity\User; +use App\Entity\JobOrder; + use App\Service\FileUploader; +use App\Service\MQTTClient; use Doctrine\ORM\Query; use Doctrine\ORM\EntityManagerInterface; @@ -18,6 +21,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; + use Catalyst\MenuBundle\Annotation\Menu; use DateTime; @@ -510,4 +515,25 @@ class RiderController extends Controller return $this->render('rider/popup.html.twig', [ 'rider' => $rider ]); } + + /** + * @ParamConverter("rider", class="App\Entity\Rider") + */ + public function riderActiveJO(EntityManagerInterface $em, MQTTClient $mclient, Rider $rider, $jo_id) + { + $jo = $em->getRepository(JobOrder::class)->find($jo_id); + $rider->setActiveJobOrder($jo); + $em->flush(); + + // TODO: trigger what needs triggering in rider app + $payload = [ + 'event' => 'cancelled', + 'reason' => 'Reprioritization', + 'jo_id' => $jo->getID(), + ]; + $mclient->sendRiderEvent($jo, $payload); + + + return $this->redirecttoRoute('rider_update', ['id' => $rider->getID()]); + } } diff --git a/src/Controller/ServiceChargeController.php b/src/Controller/ServiceChargeController.php new file mode 100644 index 00000000..7208e306 --- /dev/null +++ b/src/Controller/ServiceChargeController.php @@ -0,0 +1,268 @@ +denyAccessUnlessGranted('service_charge.list', null, 'No access.'); + + return $this->render('service-charge/list.html.twig'); + } + + public function rows(Request $req) + { + $this->denyAccessUnlessGranted('service_charge.list', null, 'No access.'); + + // build query + $qb = $this->getDoctrine() + ->getRepository(ServiceCharge::class) + ->createQueryBuilder('q'); + + // get datatable params + $datatable = $req->request->get('datatable'); + + // count total records + $tquery = $qb->select('COUNT(q)'); + + // add fitlers to count query + $this->setQueryFilters($datatable, $tquery); + + $total = $tquery->getQuery() + ->getSingleScalarResult(); + + // get current page number + $page = $datatable['pagination']['page'] ?? 1; + + $perpage = $datatable['pagination']['perpage']; + $offset = ($page - 1) * $perpage; + + // add metadata + $meta = [ + 'page' => $page, + 'perpage' => $perpage, + 'pages' => ceil($total / $perpage), + 'total' => $total, + 'sort' => 'asc', + 'field' => 'id' + ]; + + // build query + $query = $qb->select('q'); + + // add filters to query + $this->setQueryFilters($datatable, $query); + + // check if sorting is present, otherwise use default + if (isset($datatable['sort']['field']) && !empty($datatable['sort']['field'])) { + $order = $datatable['sort']['sort'] ?? 'asc'; + $query->orderBy('q.' . $datatable['sort']['field'], $order); + } else { + $query->orderBy('q.id', 'asc'); + } + + // get rows for this page + $obj_rows = $query->setFirstResult($offset) + ->setMaxResults($perpage) + ->getQuery() + ->getResult(); + + // process rows + $rows = []; + foreach ($obj_rows as $orow) { + // add row data + $row['id'] = $orow->getID(); + $row['name'] = $orow->getName(); + + // add row metadata + $row['meta'] = [ + 'update_url' => '', + 'delete_url' => '' + ]; + + // add crud urls + if ($this->isGranted('service_charge.update')) + $row['meta']['update_url'] = $this->generateUrl('service_charge_update', ['id' => $row['id']]); + if ($this->isGranted('service.delete')) + $row['meta']['delete_url'] = $this->generateUrl('service_charge_delete', ['id' => $row['id']]); + + $rows[] = $row; + } + + // response + return $this->json([ + 'meta' => $meta, + 'data' => $rows + ]); + } + + /** + * @Menu(selected="service_charge_list") + */ + public function addForm() + { + $this->denyAccessUnlessGranted('service_charge.add', null, 'No access.'); + + $params = []; + $params['obj'] = new ServiceCharge(); + $params['mode'] = 'create'; + + // response + return $this->render('service-charge/form.html.twig', $params); + } + + public function addSubmit(Request $req, ValidatorInterface $validator, EntityManagerInterface $em) + { + $this->denyAccessUnlessGranted('service_charge.add', null, 'No access.'); + + // create new object + $row = new ServiceCharge(); + + // set and save values + $row->setName($req->request->get('name')); + $row->setAmount($req->request->get('amount')); + + // validate + $errors = $validator->validate($row); + + // initialize error list + $error_array = []; + + // add errors to list + foreach ($errors as $error) { + $error_array[$error->getPropertyPath()] = $error->getMessage(); + } + + // check if any errors were found + if (!empty($error_array)) { + // return validation failure response + return $this->json([ + 'success' => false, + 'errors' => $error_array + ], 422); + } else { + // validated! save the entity + $em->persist($row); + $em->flush(); + + // return successful response + return $this->json([ + 'success' => 'Changes have been saved!' + ]); + } + } + + /** + * @Menu(selected="service_charge_list") + * @ParamConverter("sc", class="App\Entity\ServiceCharge") + */ + public function updateForm(ServiceCharge $sc) + { + $this->denyAccessUnlessGranted('service_charge.update', null, 'No access.'); + + $params = []; + $params['mode'] = 'update'; + + if ($sc == null) + throw $this->createNotFoundException('The item does not exist'); + + $params['obj'] = $sc; + + // response + return $this->render('service-charge/form.html.twig', $params); + } + + /** + * @ParamConverter("sc", class="App\Entity\ServiceCharge") + */ + public function updateSubmit(Request $req, ValidatorInterface $validator, + ServiceCharge $sc, EntityManagerInterface $em) + { + $this->denyAccessUnlessGranted('service_charge.update', null, 'No access.'); + + // make sure this row exists + if ($sc == null) + throw $this->createNotFoundException('The item does not exist'); + + // set and save values + $sc->setName($req->request->get('name')); + $sc->setAmount($req->request->get('amount')); + + // validate + $errors = $validator->validate($sc); + + // initialize error list + $error_array = []; + + // add errors to list + foreach ($errors as $error) { + $error_array[$error->getPropertyPath()] = $error->getMessage(); + } + + // check if any errors were found + if (!empty($error_array)) { + // return validation failure response + return $this->json([ + 'success' => false, + 'errors' => $error_array + ], 422); + } else { + // validated! save the entity + $em->flush(); + + // return successful response + return $this->json([ + 'success' => 'Changes have been saved!' + ]); + } + } + + /** + * @Menu(selected="service_list") + * @ParamConverter("sc", class="App\Entity\ServiceCharge") + */ + public function destroy(ServiceCharge $sc, EntityManagerInterface $em) + { + $this->denyAccessUnlessGranted('service_charge.delete', null, 'No access.'); + + $params = []; + + if ($sc == null) + throw $this->createNotFoundException('The item does not exist'); + + // delete this row + $em->remove($sc); + $em->flush(); + + // response + $response = new Response(); + $response->setStatusCode(Response::HTTP_OK); + $response->send(); + } + + protected function setQueryFilters($datatable, &$query) + { + if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) { + $query->where('q.name LIKE :filter') + ->setParameter('filter', '%' . $datatable['query']['data-rows-search'] . '%'); + } + } + +} diff --git a/src/Entity/JobOrder.php b/src/Entity/JobOrder.php index 36834cc1..9e902e98 100644 --- a/src/Entity/JobOrder.php +++ b/src/Entity/JobOrder.php @@ -280,6 +280,12 @@ class JobOrder */ protected $hub_rejections; + // meta + /** + * @ORM\Column(type="json") + */ + protected $meta; + public function __construct() { $this->date_create = new DateTime(); @@ -297,6 +303,8 @@ class JobOrder $this->trade_in_type = null; $this->flag_rider_rating = false; $this->flag_coolant = false; + + $this->meta = []; } public function getID() @@ -802,4 +810,20 @@ class JobOrder { return $this->hub_rejections; } + + public function addMeta($id, $value) + { + $this->meta[$id] = $value; + return $this; + } + + public function getMeta($id) + { + // return null if we don't have it + if (!isset($this->meta[$id])) + return null; + + return $this->meta[$id]; + } + } diff --git a/src/Entity/Rider.php b/src/Entity/Rider.php index f9331c03..53731475 100644 --- a/src/Entity/Rider.php +++ b/src/Entity/Rider.php @@ -64,6 +64,13 @@ class Rider */ protected $job_orders; + // rider's active job order since we now support multiple job orders per rider + /** + * @ORM\OneToOne(targetEntity="JobOrder") + * @ORM\JoinColumn(name="active_jo_id", referencedColumnName="id") + */ + protected $active_job_order; + // picture of rider /** * @ORM\Column(type="string", nullable=true) @@ -122,6 +129,8 @@ class Rider $this->flag_active = true; $this->username = null; $this->password = ''; + + $this->active_job_order = null; } public function getID() @@ -300,8 +309,19 @@ class Rider return $this->password; } + public function setActiveJobOrder(JobOrder $jo = null) + { + $this->active_job_order = $jo; + return $this; + } + public function getActiveJobOrder() { + // check if we have set a custom active + if ($this->active_job_order != null) + return $this->active_job_order; + + // no custom active job order $active_status = [ JOStatus::ASSIGNED, JOStatus::IN_TRANSIT, @@ -315,6 +335,20 @@ class Rider return $this->job_orders->matching($criteria)[0]; } + public function getOpenJobOrders() + { + $active_status = [ + JOStatus::ASSIGNED, + JOStatus::IN_TRANSIT, + JOStatus::IN_PROGRESS, + ]; + + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->in('status', $active_status)); + + return $this->job_orders->matching($criteria); + } + public function getSessions() { return $this->sessions; diff --git a/src/Entity/ServiceCharge.php b/src/Entity/ServiceCharge.php new file mode 100644 index 00000000..98f96c77 --- /dev/null +++ b/src/Entity/ServiceCharge.php @@ -0,0 +1,66 @@ +amount = 0; + } + + public function getID() + { + return $this->id; + } + + public function setName($name) + { + $this->name = $name; + return $this; + } + + public function getName() + { + return $this->name; + } + + public function setAmount($amount) + { + $this->amount = $amount; + return $this; + } + + public function getAmount() + { + return $this->amount; + } +} diff --git a/src/Ramcar/InvoiceCriteria.php b/src/Ramcar/InvoiceCriteria.php index 5ea5e623..6665226d 100644 --- a/src/Ramcar/InvoiceCriteria.php +++ b/src/Ramcar/InvoiceCriteria.php @@ -5,6 +5,7 @@ namespace App\Ramcar; use App\Entity\Battery; use App\Entity\Promo; use App\Entity\CustomerVehicle; +use App\Entity\ServiceCharge; class InvoiceCriteria { @@ -12,6 +13,8 @@ class InvoiceCriteria protected $promos; protected $cv; protected $flag_coolant; + protected $discount; + protected $service_charges; // entries are battery and trade-in combos protected $entries; @@ -23,6 +26,8 @@ class InvoiceCriteria $this->entries = []; $this->cv = null; $this->flag_coolant = false; + $this->discount = 0; + $this->service_charges = []; } public function setServiceType($stype) @@ -125,4 +130,27 @@ class InvoiceCriteria { return $this->flag_coolant; } + + public function setDiscount($discount) + { + $this->discount = $discount; + return $this; + } + + public function getDiscount() + { + return $this->discount; + } + + public function addServiceCharge(ServiceCharge $service_charge) + { + $this->service_charges[] = $service_charge; + return $this; + } + + public function getServiceCharges() + { + return $this->service_charges; + } + } diff --git a/src/Ramcar/TransactionOrigin.php b/src/Ramcar/TransactionOrigin.php index baf412f1..c4d69ad2 100644 --- a/src/Ramcar/TransactionOrigin.php +++ b/src/Ramcar/TransactionOrigin.php @@ -9,12 +9,15 @@ class TransactionOrigin extends NameValue const FACEBOOK = 'facebook'; const VIP = 'vip'; const MOBILE_APP = 'mobile_app'; + const WALK_IN = 'walk_in'; + // TODO: for now, resq also gets the walk-in option const COLLECTION = [ 'call' => 'Hotline', 'online' => 'Online', 'facebook' => 'Facebook', 'vip' => 'VIP', 'mobile_app' => 'Mobile App', + 'walk_in' => 'Walk-in', ]; } diff --git a/src/Service/CustomerHandler/CMBCustomerHandler.php b/src/Service/CustomerHandler/CMBCustomerHandler.php index d8107514..371f674b 100644 --- a/src/Service/CustomerHandler/CMBCustomerHandler.php +++ b/src/Service/CustomerHandler/CMBCustomerHandler.php @@ -320,7 +320,9 @@ class CMBCustomerHandler implements CustomerHandlerInterface $nerror_array = []; $verror_array = []; - // TODO: validate mobile numbers + if (!($this->validateMobileNumber($req->request->get('phone_mobile')))) + $error_array['phone_mobile'] = 'Invalid mobile phone number.'; + // TODO: validate vehicles // custom validation for vehicles @@ -685,6 +687,18 @@ class CMBCustomerHandler implements CustomerHandlerInterface } } + public function validateMobileNumber($mobile_number) + { + if (empty($mobile_number)) + return true; + if (strlen($mobile_number) != 9) + return false; + if(preg_match('/^\d+$/',$mobile_number)) + return true; + + return false; + } + // check if datatable filter is present and append to query protected function setQueryFilters($datatable, &$query) { if (isset($datatable['query']['data-rows-search']) && !empty($datatable['query']['data-rows-search'])) { diff --git a/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php b/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php index f25348a4..cc1cb020 100644 --- a/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php +++ b/src/Service/InvoiceGenerator/CMBInvoiceGenerator.php @@ -17,8 +17,8 @@ use App\Ramcar\FuelType; use App\Entity\Invoice; use App\Entity\InvoiceItem; use App\Entity\Battery; -use App\Entity\Promo; use App\Entity\User; +use App\Entity\ServiceCharge; use App\Service\InvoiceGeneratorInterface; @@ -106,9 +106,12 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface // break; } - // TODO: check if any promo is applied - // apply discounts - $promos = $criteria->getPromos(); + // process service charges if any + $service_charges = $criteria->getServiceCharges(); + if (count($service_charges) > 0) + { + $this->processServiceCharges($total, $criteria, $invoice); + } // get current user $user = $this->security->getUser(); @@ -131,7 +134,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface } // generate invoice criteria - public function generateInvoiceCriteria($jo, $promo_id, $invoice_items, &$error_array) + public function generateInvoiceCriteria($jo, $discount, $invoice_items, &$error_array) { $em = $this->em; @@ -140,7 +143,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface $criteria->setServiceType($jo->getServiceType()) ->setCustomerVehicle($jo->getCustomerVehicle()); - $ierror = $this->invoicePromo($criteria, $promo_id); + $ierror = $this->validateDiscount($criteria, $discount); if (!$ierror && !empty($invoice_items)) { @@ -158,11 +161,19 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface $ierror = $this->invoiceBatteries($criteria, $invoice_items); } + // get the meta for service charges + $service_charges = $jo->getMeta('service_charges'); + if (!empty($service_charges)) + { + $service_charges = $jo->getMeta('service_charges'); + + $this->invoiceServiceCharges($criteria, $service_charges); + } + if ($ierror) { $error_array['invoice'] = $ierror; } - else { // generate the invoice @@ -224,27 +235,26 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface return 0; } - public function invoicePromo(InvoiceCriteria $criteria, $promo_id) + public function validateDiscount(InvoiceCriteria $criteria, $discount) { + // return error if there's a problem, false otherwise // check service type $stype = $criteria->getServiceType(); if ($stype != CMBServiceType::BATTERY_REPLACEMENT_NEW) return null; - - if (empty($promo_id)) + // check if discount is blank or 0 + if ((empty($discount)) || ($discount == 0)) { return false; } - // check if this is a valid promo - $promo = $this->em->getRepository(Promo::class)->find($promo_id); + // check if discount is greater than 50 or negative number + if (($discount > 50) || ($discount < 0)) + return 'Invalid discount specified'; - if (empty($promo)) - return 'Invalid promo specified.'; - - $criteria->addPromo($promo); + $criteria->setDiscount($discount); return false; } @@ -292,6 +302,28 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface return null; } + public function invoiceServiceCharges(InvoiceCriteria $criteria, $service_charges) + { + if (!empty($service_charges)) + { + foreach ($service_charges as $service_charge) + { + // check if valid service charge + $sc = $this->em->getRepository(ServiceCharge::class)->find($service_charge['id']); + + if (empty($sc)) + { + $error = 'Invalid service charge specified.'; + return $error; + } + + $criteria->addServiceCharge($sc); + } + } + + return null; + } + protected function processEntries(&$total, InvoiceCriteria $criteria, Invoice $invoice) { @@ -389,33 +421,14 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface protected function processDiscount(&$total, InvoiceCriteria $criteria, Invoice $invoice) { - $promos = $criteria->getPromos(); - if (count($promos) < 1) - return; - - // NOTE: only get first promo because only one is applicable anyway - $promo = $promos[0]; - - $rate = $promo->getDiscountRate(); - $apply_to = $promo->getDiscountApply(); - - switch ($apply_to) - { - case DiscountApply::SRP: - $discount = round($total['sell_price'] * $rate, 2); - break; - case DiscountApply::OPL: - // $discount = round($total['sell_price'] * 0.6 / 0.7 * $rate, 2); - $discount = round($total['sell_price'] * (1 - 1.5 / 0.7 * $rate), 2); - break; - } + $discount = $criteria->getDiscount(); // if discount is higher than 0, display in invoice if ($discount > 0) { $item = new InvoiceItem(); $item->setInvoice($invoice) - ->setTitle('Promo discount') + ->setTitle('Discount') ->setQuantity(1) ->setPrice(-1 * $discount); $invoice->addItem($item); @@ -425,7 +438,7 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface $total['total_price'] -= $discount; // process - $invoice->setPromo($promo); + $invoice->setDiscount($discount); } protected function processJumpstart(&$total, $invoice) @@ -629,4 +642,24 @@ class CMBInvoiceGenerator implements InvoiceGeneratorInterface $total['vat'] = $vat; } + protected function processServiceCharges(&$total, InvoiceCriteria $criteria, Invoice $invoice) + { + $service_charges = $criteria->getServiceCharges(); + + foreach ($service_charges as $service_charge) + { + $amount = $service_charge->getAmount(); + $title = 'Service Charge - ' . $service_charge->getName(); + + $total['total_price'] += $amount; + // add item + $item = new InvoiceItem(); + $item->setInvoice($invoice) + ->setTitle($title) + ->setQuantity(1) + ->setPrice($amount); + + $invoice->addItem($item); + } + } } diff --git a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php index c5e9c81c..f9aad46a 100644 --- a/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php +++ b/src/Service/InvoiceGenerator/ResqInvoiceGenerator.php @@ -18,6 +18,7 @@ use App\Entity\Invoice; use App\Entity\InvoiceItem; use App\Entity\User; use App\Entity\Battery; +use App\Entity\Promo; use App\Service\InvoiceGeneratorInterface; diff --git a/src/Service/JobOrderHandler/CMBJobOrderHandler.php b/src/Service/JobOrderHandler/CMBJobOrderHandler.php index 40f428ee..80e4158e 100644 --- a/src/Service/JobOrderHandler/CMBJobOrderHandler.php +++ b/src/Service/JobOrderHandler/CMBJobOrderHandler.php @@ -26,6 +26,7 @@ use App\Entity\Rider; use App\Entity\JORejection; use App\Entity\Warranty; use App\Entity\Customer; +use App\Entity\ServiceCharge; use App\Ramcar\InvoiceCriteria; use App\Ramcar\CMBServiceType; @@ -42,6 +43,7 @@ use App\Ramcar\JORejectionReason; use App\Service\InvoiceGeneratorInterface; use App\Service\JobOrderHandlerInterface; use App\Service\RiderAssignmentHandlerInterface; +use App\Service\CustomerHandlerInterface; use App\Service\WarrantyHandler; use App\Service\MQTTClient; use App\Service\APNSClient; @@ -66,13 +68,15 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface protected $rah; protected $country_code; protected $wh; + protected $cust_handler; protected $template_hash; public function __construct(Security $security, EntityManagerInterface $em, InvoiceGeneratorInterface $ic, ValidatorInterface $validator, TranslatorInterface $translator, RiderAssignmentHandlerInterface $rah, - string $country_code, WarrantyHandler $wh) + string $country_code, WarrantyHandler $wh, + CustomerHandlerInterface $cust_handler) { $this->em = $em; $this->ic = $ic; @@ -82,6 +86,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $this->rah = $rah; $this->country_code = $country_code; $this->wh = $wh; + $this->cust_handler = $cust_handler; $this->loadTemplates(); } @@ -347,13 +352,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface // call service to generate job order and invoice $invoice_items = $req->request->get('invoice_items', []); - $promo_id = $req->request->get('invoice_promo'); + $discount = $req->request->get('invoice_discount'); $invoice_change = $req->request->get('invoice_change', 0); // check if invoice changed if ($invoice_change) { - $this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $error_array); + $this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array); } // validate @@ -416,8 +421,10 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $error_array['customer_customer_notes'] = 'Customer notes cannot be null.'; } - $new_cust = new Customer(); - $new_cv = new CustomerVehicle(); + // validate mobile phone + $valid_mobile = $this->cust_handler->validateMobileNumber($req->request->get('customer_phone_mobile')); + if (!($valid_mobile)) + $error_array['customer_phone_mobile'] = 'Invalid mobile phone number.'; // find the vehicle using vid $new_vehicle = $em->getRepository(Vehicle::class)->find($req->request->get('vid')); @@ -426,8 +433,10 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $error_array['cv_mfg'] = 'Invalid manufacturer specified.'; $error_array['cv_make'] = 'Invalid make specified.'; } - else + if (empty($error_array)) { + $new_cust = new Customer(); + $new_cv = new CustomerVehicle(); $new_cust->setLastName($req->request->get('customer_last_name')) ->setFirstName($req->request->get('customer_first_name')) @@ -514,6 +523,16 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface } } + // get discount and set to meta + $discount = $req->request->get('invoice_discount', []); + + // check if discount is greater than 50 or negative number + if (($discount > 50) || ($discount < 0)) + $error_array['invoice_discount'] = 'Invalid discount specified'; + + // get list of service charges + $service_charges = $req->request->get('service_charges', []); + if (empty($error_array)) { // get current user @@ -543,7 +562,10 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface ->setHub($hub) ->setRider($rider); - // check if user is null, meaning call to create came from API + $jo->addMeta('discount', $discount); + $jo->addMeta('service_charges', $service_charges); + + // check if user is null, meaning call to create came from API if ($user != null) { $jo->setCreatedBy($user); @@ -563,13 +585,13 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface // call service to generate job order and invoice $invoice_items = $req->request->get('invoice_items', []); - $promo_id = $req->request->get('invoice_promo'); + $discount = $req->request->get('invoice_discount'); $invoice_change = $req->request->get('invoice_change', 0); // check if invoice changed if ($invoice_change) { - $this->ic->generateInvoiceCriteria($jo, $promo_id, $invoice_items, $error_array); + $this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array); } // validate @@ -918,7 +940,11 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface // create the warranty if new battery only if ($this->checkIfNewBattery($obj)) { - $serial = $req->request->get('warranty_code') ; + if (empty($req->request->get('warranty_code'))) + $serial = null; + else + $serial = $req->request->get('warranty_code'); + $warranty_class = $obj->getWarrantyClass(); $first_name = $obj->getCustomer()->getFirstName(); $last_name = $obj->getCustomer()->getLastName(); @@ -1366,7 +1392,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $this->fillFormTags($params); // get template to display - $params['template'] = $this->getTwigTemplate('jo_onestep'); + $params['template'] = $this->getTwigTemplate('jo_onestep_form'); // return params return $params; @@ -1381,6 +1407,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $params['mode'] = 'onestep-edit'; $params['cvid'] = $obj->getCustomerVehicle()->getID(); $params['vid'] = $obj->getCustomerVehicle()->getVehicle()->getID(); + $params['jo_service_charges'] = $obj->getMeta('service_charges'); $this->fillDropdownParameters($params); $this->fillFormTags($params); @@ -1490,7 +1517,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface { $em = $this->em; - $params['mode'] = 'update-all'; + $params['mode'] = 'view-all'; // get row data $obj = $em->getRepository(JobOrder::class)->find($id); @@ -1502,8 +1529,12 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $this->fillDropdownParameters($params); $this->fillFormTags($params); - // get template to display - $params['template'] = $this->getTwigTemplate('jo_all_form'); + // get template to display + // check transaction origin if walkin + if ($obj->getSource() == TransactionOrigin::WALK_IN) + $params['template'] = $this->getTwigTemplate('jo_walkin_form'); + else + $params['template'] = $this->getTwigTemplate('jo_onestep_form'); $params['obj'] = $obj; $params['status_cancelled'] = JOStatus::CANCELLED; @@ -2345,6 +2376,307 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface return false; } + public function initializeWalkinForm() + { + $params['obj'] = new JobOrder(); + $params['mode'] = 'walk-in'; + + $this->fillDropdownParameters($params); + $this->fillFormTags($params); + + // get template to display + $params['template'] = $this->getTwigTemplate('jo_walkin_form'); + + // return params + return $params; + } + + public function processWalkinJobOrder(Request $req, $id) + { + // initialize error list + $error_array = []; + + $em = $this->em; + + $jo = $em->getRepository(JobOrder::class)->find($id); + if (empty($jo)) + { + // new job order + $jo = new JobOrder(); + } + + // check if new customer + if ($req->request->get('new_customer')) + { + if (empty($req->request->get('customer_customer_notes'))) + { + $error_array['customer_customer_notes'] = 'Customer notes cannot be null.'; + } + + // validate mobile phone + $valid_mobile = $this->cust_handler->validateMobileNumber($req->request->get('customer_phone_mobile')); + if (!($valid_mobile)) + $error_array['customer_phone_mobile'] = 'Invalid mobile phone number.'; + + // find the vehicle using vid + $new_vehicle = $em->getRepository(Vehicle::class)->find($req->request->get('vid')); + if (empty($new_vehicle)) + { + $error_array['cv_mfg'] = 'Invalid manufacturer specified.'; + $error_array['cv_make'] = 'Invalid make specified.'; + } + + if (empty($error_array)) + { + $new_cust = new Customer(); + $new_cv = new CustomerVehicle(); + + $new_cust->setLastName($req->request->get('customer_last_name')) + ->setFirstName($req->request->get('customer_first_name')) + ->setPhoneMobile($req->request->get('customer_phone_mobile')) + ->setPhoneLandline($req->request->get('customer_phone_landline')) + ->setPhoneOffice($req->request->get('customer_phone_office')) + ->setPhoneFax($req->request->get('customer_phone_fax')) + ->setCustomerNotes($req->request->get('customer_customer_notes')); + + $new_cv->setCustomer($new_cust) + ->setVehicle($new_vehicle) + ->setPlateNumber($req->request->get('cv_plate')) + ->setModelYear($req->request->get('cv_year')) + ->setColor('') + ->setStatusCondition('') + ->setFuelType('') + ->setActive() + ->setWarrantyCode($req->request->get('warranty_code')); + + if (($req->request->get('service_type')) == CMBServiceType::BATTERY_REPLACEMENT_NEW) + { + $new_cv->setHasMotoliteBattery(true); + } + else + { + $new_cv->setHasMotoliteBattery(false); + } + + // link JO to new customer + $jo->setCustomer($new_cust); + $jo->setCustomerVehicle($new_cv); + + $em->persist($new_cust); + $em->persist($new_cv); + } + } + else + { + // check if customer vehicle is set + if (empty($req->request->get('customer_vehicle'))) { + $error_array['customer_vehicle'] = 'No vehicle selected.'; + } else + { + // get customer vehicle + $cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($req->request->get('customer_vehicle')); + + if (empty($cust_vehicle)) { + $error_array['customer_vehicle'] = 'Invalid vehicle specified.'; + } + else + { + $jo->setCustomerVehicle($cust_vehicle); + $jo->setCustomer($cust_vehicle->getCustomer()); + + // save serial into cv + $cust_vehicle->setWarrantyCode($req->request->get('warranty_code')); + + $em->persist($cust_vehicle); + } + } + } + + // check if hub is selected + if (empty($req->request->get('hub_id'))) + $error_array['hub'] = 'No hub selected.'; + else + { + // get hub + $hub = $em->getRepository(Hub::class)->find($req->request->get('hub_id')); + + if (empty($hub)) + $error_array['hub'] = 'Invalid hub specified.'; + + // get hub coordinates + $hub_coordinates = $hub->getCoordinates(); + } + + // get discount and set to meta + $discount = $req->request->get('invoice_discount'); + + // check if discount is greater than 50 or negative number + if (($discount > 50) || ($discount < 0)) + $error_array['invoice_discount'] = 'Invalid discount specified'; + + if (empty($error_array)) + { + // get current user + $user = $this->security->getUser(); + + $stype = $req->request->get('service_type'); + + // set and save values + $jo->setDateSchedule(DateTime::createFromFormat("d M Y h:i A", $req->request->get('date_schedule_date') . " " . $req->request->get('date_schedule_time'))) + ->setAdvanceOrder($req->request->get('flag_advance') ?? false) + ->setServiceType($stype) + ->setWarrantyClass($req->request->get('warranty_class')) + ->setSource($req->request->get('source')) + ->setStatus(JOStatus::FULFILLED) + ->setTier1Notes($req->request->get('tier1_notes')) + ->setTier2Notes($req->request->get('tier2_notes')) + ->setORName($req->request->get('or_name')) + ->setPromoDetail($req->request->get('promo_detail')) + ->setModeOfPayment($req->request->get('mode_of_payment')) + ->setLandmark($req->request->get('landmark')) + ->setDeliveryAddress('Walk-in') + ->setLandmark('Walk-in') + ->setCoordinates($hub_coordinates) + ->setHub($hub); + + $jo->addMeta('discount', $discount); + + // check if user is null, meaning call to create came from API + if ($user != null) + { + $jo->setCreatedBy($user); + } + + // check if reference JO is set and validate + if (!empty($req->request->get('ref_jo'))) { + // get reference JO + $ref_jo = $em->getRepository(JobOrder::class)->find($req->request->get('ref_jo')); + + if (empty($ref_jo)) { + $error_array['ref_jo'] = 'Invalid reference job order specified.'; + } else { + $jo->setReferenceJO($ref_jo); + } + } + + // call service to generate job order and invoice + $invoice_items = $req->request->get('invoice_items', []); + $discount = $req->request->get('invoice_discount'); + $invoice_change = $req->request->get('invoice_change', 0); + + // check if invoice changed + if ($invoice_change) + { + $this->ic->generateInvoiceCriteria($jo, $discount, $invoice_items, $error_array); + } + + // validate + $errors = $this->validator->validate($jo); + + // add errors to list + foreach ($errors as $error) { + $error_array[$error->getPropertyPath()] = $error->getMessage(); + } + + // check if errors are found + if (empty($error_array)) + { + // validated, no error. save the job order + $em->persist($jo); + + // the event + $event = new JOEvent(); + $event->setDateHappen(new DateTime()) + ->setTypeID(JOEventType::CREATE) + ->setJobOrder($jo); + + if ($user != null) + { + $event->setUser($user); + } + + $em->persist($event); + + // save to customer vehicle battery record + $this->updateVehicleBattery($jo); + + // save serial to customer vehicle + $cust_vehicle = $jo->getCustomerVehicle(); + $cust_vehicle->setWarrantyCode($req->request->get('warranty_code')); + + $em->persist($cust_vehicle); + + // create the warranty if new battery only + if ($this->checkIfNewBattery($jo)) + { + if (empty($req->request->get('warranty_code'))) + $serial = null; + else + $serial = $req->request->get('warranty_code'); + + $warranty_class = $jo->getWarrantyClass(); + $first_name = $jo->getCustomer()->getFirstName(); + $last_name = $jo->getCustomer()->getLastName(); + $mobile_number = $jo->getCustomer()->getPhoneMobile(); + + // check if date fulfilled is null + if ($jo->getDateFulfill() == null) + $date_purchase = $jo->getDateCreate(); + else + $date_purchase = $jo->getDateFulfill(); + + // validate plate number + // $plate_number = $this->wh->cleanPlateNumber($jo->getCustomerVehicle()->getPlateNumber()); + $plate_number = Warranty::cleanPlateNumber($jo->getCustomerVehicle()->getPlateNumber()); + if ($plate_number != false) + { + $batt_list = array(); + $invoice = $jo->getInvoice(); + if (!empty($invoice)) + { + // get battery + $invoice_items = $invoice->getItems(); + foreach ($invoice_items as $item) + { + $battery = $item->getBattery(); + if ($battery != null) + { + $batt_list[] = $item->getBattery(); + } + } + } + + $this->wh->createWarranty($serial, $plate_number, $first_name, $last_name, $mobile_number, $batt_list, $date_purchase, $warranty_class); + } + } + + $em->flush(); + } + } + + return $error_array; + } + + public function initializeWalkinEditForm($id) + { + $em = $this->em; + $obj = $em->getRepository(JobOrder::class)->find($id); + + $params['obj'] = $obj; + $params['mode'] = 'walk-in-edit'; + $params['cvid'] = $obj->getCustomerVehicle()->getID(); + $params['vid'] = $obj->getCustomerVehicle()->getVehicle()->getID(); + + $this->fillDropdownParameters($params); + $this->fillFormTags($params); + + // get template to display + $params['template'] = $this->getTwigTemplate('jo_walkin_edit_form'); + + // return params + return $params; + } + protected function fillDropdownParameters(&$params) { $em = $this->em; @@ -2352,6 +2684,7 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface // db loaded $params['bmfgs'] = $em->getRepository(BatteryManufacturer::class)->findAll(); $params['promos'] = $em->getRepository(Promo::class)->findAll(); + $params['service_charges'] = $em->getRepository(ServiceCharge::class)->findAll(); // list of hubs $hubs = $em->getRepository(Hub::class)->findBy([], ['name' => 'ASC']); @@ -2422,6 +2755,18 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $params['ftags']['invoice_edit'] = true; $params['ftags']['preset_vehicle'] = true; break; + case 'walk-in': + $params['ftags']['vehicle_dropdown'] = true; + $params['ftags']['set_map_coordinate'] = false; + $params['ftags']['invoice_edit'] = true; + $params['ftags']['ticket_table'] = false; + $params['ftags']['cancel_button'] = false; + break; + case 'walk-in-edit': + $params['ftags']['invoice_edit'] = true; + $params['ftags']['preset_vehicle'] = true; + break; + } } @@ -2448,8 +2793,10 @@ class CMBJobOrderHandler implements JobOrderHandlerInterface $this->template_hash['jo_list_fulfillment'] = 'job-order/list.fulfillment.html.twig'; $this->template_hash['jo_list_open'] = 'job-order/list.open.html.twig'; $this->template_hash['jo_list_all'] = 'job-order/list.all.html.twig'; - $this->template_hash['jo_onestep'] = 'job-order/cmb.form.onestep.html.twig'; + $this->template_hash['jo_onestep_form'] = 'job-order/cmb.form.onestep.html.twig'; $this->template_hash['jo_onestep_edit_form'] = 'job-order/cmb.form.onestep.html.twig'; + $this->template_hash['jo_walkin_form'] = 'job-order/cmb.form.walkin.html.twig'; + $this->template_hash['jo_walkin_edit_form'] = 'job-order/cmb.form.walkin.html.twig'; } protected function checkTier($tier) diff --git a/templates/job-order/cmb.form.onestep.html.twig b/templates/job-order/cmb.form.onestep.html.twig index e0714676..0d9bac9f 100644 --- a/templates/job-order/cmb.form.onestep.html.twig +++ b/templates/job-order/cmb.form.onestep.html.twig @@ -389,14 +389,20 @@

- Nearest Hubs + {% if mode == 'view-all' %} + Assigned Hub + {% else %} + Nearest Hubs + {% endif %}

- + {% if mode != 'view-all' %} + + {% endif %}
@@ -430,14 +436,20 @@

- Rider Assignment + {% if mode == 'view-all' %} + Assigned Rider + {% else %} + Rider Assignment + {% endif %}

- + {% if mode != 'view-all' %} + + {% endif %}
@@ -452,7 +464,7 @@ - {% if mode in ['onestep-edit'] %} + {% if mode in ['onestep-edit', 'view-all'] %} {% set avail_riders = obj.getHub.getAvailableRiders|default([]) %} @@ -462,7 +474,11 @@ {% if obj.getHub %} {% for rider in avail_riders %} - + {% if mode == 'view-all' %} + + {% else %} + + {% endif %} {{ rider.getFirstName }} {{ rider.getLastName }} {{ rider.getContactNumber }} @@ -477,6 +493,42 @@
+
+
+
+

+ Service Charges +

+
+
+ {% if mode != 'view-all' %} +
+ +
+ {% endif %} +
+ + {% for jo_sc_key, jo_sc in obj.getMeta('service_charges')|default([]) %} +
+
+
+ +
+
+
+ +
+
+ +
+
+ {% endfor %} +
+
@@ -498,24 +550,15 @@
- + {% if ftags.invoice_edit %} - - + + {% else %} - + {% endif %}
-
- - -
-
+
@@ -609,9 +652,11 @@
- - {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} - Cancel Job Order + {% if mode != 'view-all' %} + + {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} + Cancel Job Order + {% endif %} {% endif %} Back
@@ -756,6 +801,32 @@ $(function() { }); {% endif %} + {% if mode in ['view-all'] %} + var hub_table = ''; + $.getJSON("{{ url('hub_nearest') }}?lat=" + lat + "&long=" + lng, function(data) { + var hubs = data['hubs']; + var hub_marker; + for (i in hubs) { + var hub = hubs[i]; + + if(selected_hub == hub['id']) { + hub_table += ''; + hub_marker = L.marker([hub['lat'], hub['long']], { icon: icon_hub }); + hubLayerGroup.addLayer(hub_marker); + + hub_table += '' + hub['name'] + ''; + hub_table += '' + hub['branch'] + ''; + hub_table += '' + hub['cnum'] + ''; + hub_table += '' + hub['distance'] + ''; + hub_table += ''; + hub_table += ''; + } + } + + $('#nearest_hubs').html(hub_table); + }); + {% endif %} + // add marker to layer group markerLayerGroup.addLayer(marker); @@ -819,76 +890,80 @@ $(function() { }); $(function() { - $('#hubs-table').on('click', 'tr', function() { - var id = $(this).data('id'); + {% if mode != 'view-all' %} + $('#hubs-table').on('click', 'tr', function() { + var id = $(this).data('id'); - riderLayerGroup.clearLayers(); + riderLayerGroup.clearLayers(); - if (id != selected_hub) { + if (id != selected_hub) { - // highlight this row - $('#hubs-table').find('.m-table__row--primary').removeClass('m-table__row--primary'); + // highlight this row + $('#hubs-table').find('.m-table__row--primary').removeClass('m-table__row--primary'); - $(this).addClass('m-table__row--primary'); + $(this).addClass('m-table__row--primary'); - // set hub - selected_hub = id; - $('#hub-field').val(selected_hub); + // set hub + selected_hub = id; + $('#hub-field').val(selected_hub); - // clear rider field - $('#rider-field').val(''); - selected_rider = ''; + // clear rider field + $('#rider-field').val(''); + selected_rider = ''; - // get riders of hub - // get hub riders ajax - // TODO: add latitude and longitude of delivery location to ajax request - var rider_table = ''; - $.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) { - var riders = data['riders']; - for (i in riders) { - var rider = riders[i]; - var rider_lat = rider['location'][0]; - var rider_lng = rider['location'][1]; - var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available }); - riderLayerGroup.addLayer(rider_marker); + // get riders of hub + // get hub riders ajax + // TODO: add latitude and longitude of delivery location to ajax request + var rider_table = ''; + $.getJSON("{{ url('hub_riders') }}?id=" + selected_hub, function(data) { + var riders = data['riders']; + for (i in riders) { + var rider = riders[i]; + var rider_lat = rider['location'][0]; + var rider_lng = rider['location'][1]; + var rider_marker = L.marker([rider_lat, rider_lng], { icon: icon_rider_available }); + riderLayerGroup.addLayer(rider_marker); - rider_table += ''; - rider_table += '' + rider['first_name'] + ''; - rider_table += '' + rider['last_name'] + ''; - rider_table += '' + rider['contact_num'] + ''; - rider_table += '' + rider['plate_num'] + ''; - rider_table += ''; - rider_table += ''; - } + rider_table += ''; + rider_table += '' + rider['first_name'] + ''; + rider_table += '' + rider['last_name'] + ''; + rider_table += '' + rider['contact_num'] + ''; + rider_table += '' + rider['plate_num'] + ''; + rider_table += ''; + rider_table += ''; + } - $('#riders').html(rider_table); - }); - } else { - // unhighlight this row - $(this).removeClass('m-table__row--primary'); - - // remove id value - selected_hub = ''; - } - }); + $('#riders').html(rider_table); + }); + } else { + // unhighlight this row + $(this).removeClass('m-table__row--primary'); + + // remove id value + selected_hub = ''; + } + }); + {% endif %} }); $(function() { - $('#rider-table').on('click', 'tr', function() { - var id = $(this).data('id'); + {% if mode != 'view-all' %} + $('#rider-table').on('click', 'tr', function() { + var id = $(this).data('id'); - // highlight this row - $('#rider-table').find('.m-table__row--primary').removeClass('m-table__row--primary'); + // highlight this row + $('#rider-table').find('.m-table__row--primary').removeClass('m-table__row--primary'); - $(this).addClass('m-table__row--primary'); + $(this).addClass('m-table__row--primary'); - // set rider - selected_rider = id; - $('#rider-field').val(selected_rider); - }); + // set rider + selected_rider = id; + $('#rider-field').val(selected_rider); + }); + {% endif %} }); - {% if mode in ['onestep-edit'] %} + {% if mode in ['onestep-edit', 'view-all'] %} var lat = {{ obj.getCoordinates.getLatitude }}; var lng = {{ obj.getCoordinates.getLongitude }}; @@ -969,6 +1044,9 @@ $(function() { // add invoice items to data fields['invoice_items'] = invoiceItems; + // add service charges to data + fields['service_charges'] = sc_array; + {% if mode in ['update-processing', 'update-reassign-hub'] %} // add selected hub to data fields['hub'] = selectedHub; @@ -1276,6 +1354,22 @@ $(function() { }); var invoiceItems = []; + var sc_array = []; + + // populate invoiceItems if editing so that we don't lose the battery + {% if mode in ['view-all', 'open-edit', 'onestep-edit', 'walk-in-edit'] %} + {% if (obj.getInvoice and obj.getInvoice.getItems|length > 0) %} + {% for item in obj.getInvoice.getItems %} + {% if item.getBattery() %} + invoiceItems.push({ + battery: {{ item.getBattery().getID() }}, + quantity: {{ item.getQuantity() }}, + trade_in: {{ obj.getInvoice().getTradeIn }}, + }); + {% endif %} + {% endfor %} + {% endif %} + {% endif %} // add to invoice $("#btn-add-to-invoice").click(function() { @@ -1316,7 +1410,7 @@ $(function() { }); // update invoice when promo is changed - $("#invoice-promo").change(function() { + $("#invoice-discount").change(function() { generateInvoice(); }); @@ -1325,72 +1419,84 @@ $(function() { generateInvoice(); }); - // reset the invoice table - $("#btn-reset-invoice").click(function() { - $("#invoice-promo").prop('selectedIndex', 0); - invoiceItems = []; - generateInvoice(); - }); + // reset the invoice table + $("#btn-reset-invoice").click(function() { + $("#invoice-discount").val(0); + $('.sc-select').closest('.row').remove(); + invoiceItems = []; + generateInvoice(); + }); // recompute $("#btn-recompute-invoice").click(function() { generateInvoice(); }); - function generateInvoice() { - var promo = $("#invoice-promo").val(); - var table = $("#invoice-table tbody"); + function generateInvoice() { + var discount = $("#invoice-discount").val(); + var table = $("#invoice-table tbody"); var stype = $("#service_type").val(); var cvid = $("#customer-vehicle").val(); - // generate invoice values - $.ajax({ - method: "POST", - url: "{{ url('jo_gen_invoice') }}", - data: { + sc_array = []; + + // get the service charges + $('.sc-select').each(function() { + var id = $(this).children('option:selected').val(); + sc_array.push({ + id: id, + }); + }); + + // generate invoice values + $.ajax({ + method: "POST", + url: "{{ url('jo_gen_invoice') }}", + data: { 'stype': stype, - 'items': invoiceItems, - 'promo': promo, - 'cvid': cvid - } - }).done(function(response) { + 'items': invoiceItems, + 'promo': discount, + 'cvid': cvid, + 'service_charges': sc_array, + } + }).done(function(response) { // mark as invoice changed $("#invoice-change").val(1); - var invoice = response.invoice; + var invoice = response.invoice; - // populate totals - $("#invoice-promo-discount").val(invoice.discount); - $("#invoice-price").val(invoice.price); - $("#invoice-trade-in").val(invoice.trade_in); - $("#invoice-vat").val(invoice.vat); - $("#invoice-total-amount").val(invoice.total_price); + // populate totals + $("#invoice-discount").val(invoice.discount); + $("#invoice-price").val(invoice.price); + $("#invoice-trade-in").val(invoice.trade_in); + $("#invoice-vat").val(invoice.vat); + $("#invoice-total-amount").val(invoice.total_price); - // populate rows - var html = ''; + // populate rows + var html = ''; - if (invoice.items.length > 0) { - $.each(invoice.items, function(key, item) { - html += '' + - '' + item.title + '' + - '' + item.quantity + '' + - '' + item.unit_price + '' + - '' + item.amount + '' + - /* - '' + - */ - ''; - }); - } else { - html = '' + - '' + - 'No items to display.' + - '' + - ''; - } - - table.html(html); - }); - } + if (invoice.items.length > 0) { + $.each(invoice.items, function(key, item) { + html += '' + + '' + item.title + '' + + '' + item.quantity + '' + + '' + item.unit_price + '' + + '' + item.amount + '' + + /* + '' + + */ + ''; + }); + } else { + html = '' + + '' + + 'No items to display.' + + '' + + ''; + } + + table.html(html); + }); + } // remove from invoice table // TODO: figure out a way to delete rows, and should deleting trade ins be allowed since they count as items on the table? @@ -1601,6 +1707,54 @@ $(function() { }); }); }); + + // service charge add + $('#btn-sc-add').click(function(e) { + console.log('adding service charge'); + // add dropdown before the button + var html = '
'; + html += '
'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + + $('#sc-section').append(html); + + // trigger change in select + $('#sc-section').find('.sc-select').last().change(); + return false; + }); + + // service charge remove + $('body').on('click', '.btn-sc-remove', function(e) { + console.log('removing service charge'); + + $(this).closest('.row').remove(); + + generateInvoice(); + + return false; + }); + + $('body').on('change', '.sc-select', function(e) { + var amount = $(this).children('option:selected').data('amount'); + $(this).closest('.row').find('.sc-amount').val(amount); + + generateInvoice(); + }); + }); {% endblock %} diff --git a/templates/job-order/cmb.form.walkin.html.twig b/templates/job-order/cmb.form.walkin.html.twig new file mode 100644 index 00000000..f2e02e44 --- /dev/null +++ b/templates/job-order/cmb.form.walkin.html.twig @@ -0,0 +1,1101 @@ +{% extends 'base.html.twig' %} + +{% block body %} + + + +
+ +
+
+
+
+
+
+ + + +

+ Walk-in Job Order +

+
+
+
+
+ + +
+ {%if ftags.vehicle_dropdown %} +
+
+
+ + + +
+ +
+
+
+
+
+ + + +
+
+
+ {% else %} + + {% endif %} + {% if obj.getReferenceJO %} +
+
+
+ + + +
+
+
+ {% endif %} + +
+
+

+ Customer Details +

+ + + +
+
+
+ + + +
+
+ + + +
+
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+ +
+ {% trans %}country_code_prefix{% endtrans %} + + +
+
+
+
+
+ + + +
+
+
+
+
+

+ Vehicle Details +

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+ +
+
+
+
+

+ Battery Details +

+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+
+

+ Transaction Details +

+ + + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+ + + +
+
+ +
+ + + + +
+ +
+
+ +
+ + + + +
+ +
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+
+
+
+

+ Hubs +

+
+
+
+ {% if mode != 'view-all' %} + + {% endif %} + + +
+ + + + + + + + + + + + {% if mode == 'view-all' %} + + + + + + + {% else %} + {% for hub in hubs %} + + + + + + + {% endfor %} + {% endif %} + +
HubBranchContact NumbersDistance in KMAction
{{ obj.getHub.getName }}{{ obj.getHub.getBranch }}{{ obj.getHub.getContactNumbers }}
{{ hub.getName }}{{ hub.getBranch }}{{ hub.getContactNumbers }}
+
+
+
+
+
+
+
+

+ Invoice +

+
+
+
+ + + +
+
+ + + +
+
+
+
+ + {% if ftags.invoice_edit %} + + + {% else %} + + {% endif %} +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + + + + + + + + {% if not obj.getInvoice or (obj.getInvoice and obj.getInvoice.getItems|length == 0) %} + + + + {% else %} + {% for item in obj.getInvoice.getItems %} + + + + + + + {% endfor %} + {% endif %} + +
ItemQuantityUnit PriceAmount
+ No items to display. +
{{ item.getTitle }}{{ item.getQuantity|number_format }}{{ item.getPrice|number_format(2) }}{{ (item.getPrice * item.getQuantity)|number_format(2) }}
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+
+
+
+
+
+
+ {% if mode != 'view-all' %} + + {% if ftags.set_map_coordinate and is_granted('joborder.cancel') and not obj.isCancelled %} + Cancel Job Order + {% endif %} + {% endif %} + Back +
+
+
+
+ +
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} + diff --git a/templates/rider/form.html.twig b/templates/rider/form.html.twig index c08c250c..a9b0264c 100644 --- a/templates/rider/form.html.twig +++ b/templates/rider/form.html.twig @@ -13,7 +13,7 @@
-
+
@@ -150,6 +150,56 @@ {% endif %}
+
+
+
+

+ Active Job Orders +

+
+
+
+ + + + + + + + + + + + + {% set active_jo_id = obj.getActiveJobOrder.getID|default(0) %} + {% for jo in obj.getOpenJobOrders %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
IDDateCustomerLocationQ StatusAction
{{ jo.getID }}{{ jo.getDateSchedule.format('Y-m-d H:i:s') }}{{ jo.getCustomer.getNameDisplay }}{{ jo.getDeliveryAddress|default('') }}{% if jo.getID == active_jo_id %}Active{% endif %} + {% if jo.getID != active_jo_id %} + + + + + + + {% endif %} +
No assigned job orders.
+
+
+
diff --git a/templates/service-charge/form.html.twig b/templates/service-charge/form.html.twig new file mode 100644 index 00000000..73b5750a --- /dev/null +++ b/templates/service-charge/form.html.twig @@ -0,0 +1,143 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

Service Charges

+
+
+
+ +
+ +
+
+
+
+
+
+ + + +

+ {% if mode == 'update' %} + Edit Service Charge + {{ obj.getID() }} + {% else %} + New Service Charge + {% endif %} +

+
+
+
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+ +
+
+
+
+
+ + Back +
+
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/service-charge/list.html.twig b/templates/service-charge/list.html.twig new file mode 100644 index 00000000..6f92e838 --- /dev/null +++ b/templates/service-charge/list.html.twig @@ -0,0 +1,142 @@ +{% extends 'base.html.twig' %} + +{% block body %} + +
+
+
+

+ Service Charges +

+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %}