diff --git a/public/assets/css/style.css b/public/assets/css/style.css index a67f0745..db831654 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1,7 +1,9 @@ /* template add-ons */ label.has-danger, -.form-control-feedback.has-danger { +.form-control-feedback.has-danger, +span.has-danger, +.nav-link.has-danger { color: #f4516c !important; } @@ -44,4 +46,10 @@ label.has-danger, .switch-label { margin: 6px 0 0 8px; +} + +@media (min-width: 995px) { + .modal-lg { + max-width: 1024px; + } } \ No newline at end of file diff --git a/src/Controller/CustomerController.php b/src/Controller/CustomerController.php index bc2d9ffc..1be54743 100644 --- a/src/Controller/CustomerController.php +++ b/src/Controller/CustomerController.php @@ -171,7 +171,8 @@ class CustomerController extends BaseController foreach ($numbers as $key => $number) { $mobile_number = new MobileNumber(); $mobile_number->setID($number->id) - ->setDateRegistered(DateTime::createFromFormat("d M Y - h:i A", $number->date_registered)); + ->setDateRegistered(DateTime::createFromFormat("d M Y - h:i A", $number->date_registered)) + ->setCustomer($row); if (!empty($number->date_confirmed)) { $mobile_number->setDateConfirmed(DateTime::createFromFormat("d M Y - h:i A", $number->date_confirmed)) @@ -190,8 +191,7 @@ class CustomerController extends BaseController $nerror_array[$key][$error->getPropertyPath()] = $error->getMessage(); } - // add to entity - if (!$nerrors) { + if (!isset($nerror_array[$key])) { $row->addMobileNumber($mobile_number); } } @@ -209,16 +209,18 @@ class CustomerController extends BaseController $verror_array[$vehicle->index]['vehicle'] = 'Invalid vehicle specified.'; } else { $cust_vehicle = new CustomerVehicle(); - $cust_vehicle->setVehicle($vobj) + $cust_vehicle->setName($vehicle->name) + ->setVehicle($vobj) ->setPlateNumber($vehicle->plate_number) ->setModelYear($vehicle->model_year) ->setColor($vehicle->color) ->setStatusCondition($vehicle->status_condition) ->setFuelType($vehicle->fuel_type) - ->setActive($vehicle->flag_active); + ->setActive($vehicle->flag_active) + ->setCustomer($row); // if specified, check if battery exists - if (isset($vehicle->battery)) { + if ($vehicle->battery) { // check if battery exists $bobj = $em->getRepository(Battery::class)->find($vehicle->battery); @@ -237,15 +239,15 @@ class CustomerController extends BaseController $verrors = $validator->validate($cust_vehicle); // add errors to list - foreach ($nerrors as $error) { - if (!isset($nerror_array[$key])) + foreach ($verrors as $error) { + if (!isset($verror_array[$vehicle->index])) $verror_array[$vehicle->index] = []; $verror_array[$vehicle->index][$error->getPropertyPath()] = $error->getMessage(); } // add to entity - if (empty($verror_array[$vehicle->index])) { + if (!isset($verror_array[$vehicle->index])) { $row->addVehicle($cust_vehicle); } } @@ -319,28 +321,180 @@ class CustomerController extends BaseController throw $this->createNotFoundException('The item does not exist'); // set and save values - $row->setName($req->request->get('name')); + $row->setFirstName($req->request->get('first_name')) + ->setLastName($req->request->get('last_name')); + + // initialize error lists + $error_array = []; + $nerror_array = []; + $verror_array = []; + + // initialize child id lists + $number_ids = []; + $vehicle_ids = []; + + // custom validation for mobile numbers + $numbers = json_decode($req->request->get('mobile_numbers')); + + if (!empty($numbers)) { + foreach ($numbers as $key => $number) { + // check if number exists + $mobile_number = $em->getRepository(MobileNumber::class)->find($number->id); + + // this is a new number + if (empty($mobile_number)) { + $mobile_number = new MobileNumber(); + $mobile_number->setID($number->id) + ->setDateRegistered(DateTime::createFromFormat("d M Y - h:i A", $number->date_registered)) + ->setCustomer($row); + + if (!empty($number->date_confirmed)) { + $mobile_number->setDateConfirmed(DateTime::createFromFormat("d M Y - h:i A", $number->date_confirmed)) + ->setConfirmed(); + } else { + $mobile_number->setConfirmed(false); + } + + $nerrors = $validator->validate($mobile_number); + + // add errors to list + foreach ($nerrors as $error) { + if (!isset($nerror_array[$key])) + $nerror_array[$key] = []; + + $nerror_array[$key][$error->getPropertyPath()] = $error->getMessage(); + } + + if (!isset($nerror_array[$key])) { + $row->addMobileNumber($mobile_number); + } + } + + // add to list of numbers to keep + $number_ids[] = $mobile_number->getID(); + } + } + + // delete all numbers not in list + $qb = $em->createQueryBuilder(); + $del_numbers = $qb->select('m') + ->from(MobileNumber::class, 'm') + ->where($qb->expr()->notIn('m.id', $number_ids)) + ->getQuery() + ->getResult(); + + if (!empty($del_numbers)) { + foreach ($del_numbers as $dn) { + $em->remove($dn); + } + } + + // custom validation for vehicles + $vehicles = json_decode($req->request->get('vehicles')); + + if (!empty($vehicles)) { + foreach ($vehicles as $vehicle) { + $cust_vehicle = false; + $is_new_vehicle = false; + + // check if customer vehicle exists + if (!empty($vehicle->id)) { + $cust_vehicle = $em->getRepository(CustomerVehicle::class)->find($vehicle->id); + } + + // this is a new vehicle + if (empty($cust_vehicle)) { + $cust_vehicle = new CustomerVehicle(); + $cust_vehicle->setCustomer($row); + $is_new_vehicle = true; + } + + // check if vehicle exists + $vobj = $em->getRepository(Vehicle::class)->find($vehicle->vehicle); + + if (empty($vobj)) { + $verror_array[$vehicle->index]['vehicle'] = 'Invalid vehicle specified.'; + } else { + $cust_vehicle->setName($vehicle->name) + ->setVehicle($vobj) + ->setPlateNumber($vehicle->plate_number) + ->setModelYear($vehicle->model_year) + ->setColor($vehicle->color) + ->setStatusCondition($vehicle->status_condition) + ->setFuelType($vehicle->fuel_type) + ->setActive($vehicle->flag_active); + + // if specified, check if battery exists + if ($vehicle->battery) { + // check if battery exists + $bobj = $em->getRepository(Battery::class)->find($vehicle->battery); + + if (empty($bobj)) { + $verror_array[$vehicle->index]['battery'] = 'Invalid battery specified.'; + } else { + $cust_vehicle->setHasMotoliteBattery(true) + ->setCurrentBattery($bobj) + ->setWarrantyCode($vehicle->warranty_code) + ->setWarrantyExpiration(DateTime::createFromFormat("d M Y", $vehicle->warranty_expiration)); + } + } else { + $cust_vehicle->setHasMotoliteBattery(false); + } + + $verrors = $validator->validate($cust_vehicle); + + // add errors to list + foreach ($verrors as $error) { + if (!isset($verror_array[$vehicle->index])) + $verror_array[$vehicle->index] = []; + + $verror_array[$vehicle->index][$error->getPropertyPath()] = $error->getMessage(); + } + + if (!isset($verror_array[$vehicle->index]) && $is_new_vehicle) { + $row->addVehicle($cust_vehicle); + } + + // add to list of vehicles to keep + $vehicle_ids[] = $cust_vehicle->getID(); + } + } + } + + // delete all vehicles not in list + $qb = $em->createQueryBuilder(); + $del_vehicles = $qb->select('cv') + ->from(CustomerVehicle::class, 'cv') + ->where($qb->expr()->notIn('cv.id', $vehicle_ids)) + ->getQuery() + ->getResult(); + + if (!empty($del_vehicles)) { + foreach ($del_vehicles as $dv) { + $em->remove($dv); + } + } // 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)) { + if (!empty($error_array) || !empty($nerror_array) || !empty($verror_array)) { // return validation failure response return $this->json([ 'success' => false, - 'errors' => $error_array + 'errors' => $error_array, + 'nerrors' => $nerror_array, + 'verrors' => $verror_array ], 422); } else { - // validated! save the entity + // validated! save the entity. do a persist anyway to save child entities + $em->persist($row); $em->flush(); // return successful response diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index d968691f..c1cd86cd 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -36,7 +36,7 @@ class Customer // mobile numbers linked to this customer /** - * @ORM\OneToMany(targetEntity="MobileNumber", mappedBy="customer") + * @ORM\OneToMany(targetEntity="MobileNumber", mappedBy="customer", cascade={"persist"}) */ protected $numbers; @@ -48,7 +48,7 @@ class Customer // vehicles linked to customer /** - * @ORM\OneToMany(targetEntity="CustomerVehicle", mappedBy="customer") + * @ORM\OneToMany(targetEntity="CustomerVehicle", mappedBy="customer", cascade={"persist"}) */ protected $vehicles; @@ -133,7 +133,7 @@ class Customer return $this->sessions; } - public function addVehicle(Vehicle $vehicle) + public function addVehicle(CustomerVehicle $vehicle) { $this->vehicles->add($vehicle); return $this; diff --git a/src/Entity/CustomerVehicle.php b/src/Entity/CustomerVehicle.php index ab29b26c..60271608 100644 --- a/src/Entity/CustomerVehicle.php +++ b/src/Entity/CustomerVehicle.php @@ -23,6 +23,7 @@ class CustomerVehicle // user-defined name for vehicle /** * @ORM\Column(type="string", length=80) + * @Assert\NotBlank() */ protected $name; @@ -30,6 +31,7 @@ class CustomerVehicle /** * @ORM\ManyToOne(targetEntity="Customer", inversedBy="vehicles") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") + * @Assert\NotBlank() */ protected $customer; @@ -80,7 +82,6 @@ class CustomerVehicle // TODO: figure out how to check expiration /** * @ORM\Column(type="string", length=20, nullable=true) - * @Assert\NotBlank() */ protected $warranty_code; @@ -120,6 +121,17 @@ class CustomerVehicle return $this->id; } + public function setName($name) + { + $this->name = $name; + return $this; + } + + public function getName() + { + return $this->name; + } + public function setCustomer(Customer $customer) { $this->customer = $customer; @@ -241,14 +253,14 @@ class CustomerVehicle return $this->flag_motolite_battery; } - public function setActive($active = true) + public function setActive($flag_active = true) { - $this->active = $active; + $this->flag_active = $flag_active; return $this; } public function isActive() { - return $this->active; + return $this->flag_active; } } diff --git a/src/Entity/MobileNumber.php b/src/Entity/MobileNumber.php index ea402acc..586a5392 100644 --- a/src/Entity/MobileNumber.php +++ b/src/Entity/MobileNumber.php @@ -28,6 +28,7 @@ class MobileNumber /** * @ORM\ManyToOne(targetEntity="Customer", inversedBy="numbers") * @ORM\JoinColumn(name="customer_id", referencedColumnName="id") + * @Assert\NotBlank() */ protected $customer; diff --git a/templates/customer/form.html.twig b/templates/customer/form.html.twig index 31d18576..6f76c35b 100644 --- a/templates/customer/form.html.twig +++ b/templates/customer/form.html.twig @@ -34,7 +34,7 @@
-
@@ -140,7 +140,13 @@
-
+
+ + + + Display name for this vehicle +
+
-
+
-
+
-
+
-
+
-
+
-
-
-
+
-
- +
+
+
-
+
-
+
-
-
-
+
-
+
@@ -315,7 +318,10 @@ } }); }).fail(function(response) { - var errors = response.responseJSON.errors; + var json = response.responseJSON; + var errors = json.errors; + var nerrors = json.nerrors; + var verrors = json.verrors; var firstfield = false; // remove all error classes first @@ -323,7 +329,7 @@ // display errors contextually $.each(errors, function(field, msg) { - var formfield = $("[name='" + field + "']"); + var formfield = $("[data-name='" + field + "']"); var label = $("label[data-field='" + field + "']"); var msgbox = $(".form-control-feedback[data-field='" + field + "']"); @@ -338,8 +344,66 @@ if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) { firstfield = domfield; } + + // add error class to this tab + var tabId = $(formfield).closest('.tab-pane').prop('id'); + $("#customer-tabs").find("a[href='#" + tabId + "']").addClass('has-danger'); }); + // loop through mobile number errors + $.each(nerrors, function(rowindex, errorlist) { + var row = $("#data-mobile-numbers table").find("[name='index'][value='" + rowindex + "']").closest('tr'); + + $.each(errorlist, function(field, msg) { + var msgbox = row.find(".form-control-feedback[data-field='" + field + "']"); + var col = msgbox.closest('span'); + + // add error classes to bad fields + col.addClass('has-danger'); + msgbox.html(msg).addClass('has-danger').removeClass('hide'); + + // check if this field comes first in DOM + var domfield = col.get(0); + + if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) { + firstfield = domfield; + } + + // add error class to this tab + var tabId = $(col).closest('.tab-pane').prop('id'); + $("#customer-tabs").find("a[href='#" + tabId + "']").addClass('has-danger'); + }); + }); + + // loop through vehicle errors + $.each(verrors, function(rowindex, errorlist) { + var row = $("#data-vehicles table").find("[name='index'][value='" + rowindex + "']").closest('tr'); + + $.each(errorlist, function(field, msg) { + var msgbox = row.find(".form-control-feedback[data-field='" + field + "']"); + var col = msgbox.closest('span'); + + // add error classes to bad fields + col.addClass('has-danger'); + msgbox.html(msg).addClass('has-danger').removeClass('hide'); + + // check if this field comes first in DOM + var domfield = col.get(0); + + if (!firstfield || (firstfield && firstfield.compareDocumentPosition(domfield) === 2)) { + firstfield = domfield; + } + + // add error class to this tab + var tabId = $(col).closest('.tab-pane').prop('id'); + $("#customer-tabs").find("a[href='#" + tabId + "']").addClass('has-danger'); + }); + }); + + // focus on tab containing first field + var firstTabId = $(firstfield).closest('.tab-pane').prop('id'); + $("#customer-tabs").find("a[href='#" + firstTabId + "']").tab('show'); + // focus on first bad field firstfield.focus(); @@ -353,7 +417,7 @@ // remove all error classes function removeErrors() { $(".form-control-danger").removeClass('form-control-danger'); - $("[data-field]").removeClass('has-danger'); + $(".has-danger").removeClass('has-danger'); $(".form-control-feedback[data-field]").addClass('hide'); } @@ -399,8 +463,8 @@ {% for number in obj.getMobileNumbers() %} nrow = { id: "{{ number.getID() }}", - date_registered: "{{ number.getDateRegistered() }}", - date_confirmed: "{{ number.getDateConfirmed() }}" + date_registered: "{{ number.getDateRegistered()|date('d M Y - h:i A') }}", + date_confirmed: "{{ number.getDateConfirmed()|date('d M Y - h:i A') }}" }; numberRows.push(nrow); @@ -413,20 +477,21 @@ vrow = { id: "{{ cv.getID() }}", + name: "{{ cv.getName() }}", index: moment().unix(), - battery: "{{ battery.getID() }}", - bmfg: "{{ battery.getManufacturer().getID() }}", + battery: "{{ battery ? battery.getID() }}", + battery_label: "{{ battery ? battery.getModel().getName() ~ ' ' ~ battery.getSize().getName() ~ ' (' ~ battery.getProductCode() ~ ')' }}", + bmfg: "{{ battery ? battery.getManufacturer().getID() }}", color: "{{ cv.getColor() }}", - flag_active: {{ cv.isActive() }}, - flag_motolite_battery: {{ cv.hasMotoliteBattery() }}, + flag_active: {{ cv.isActive() ? 'true' : 'false' }}, + flag_motolite_battery: {{ cv.hasMotoliteBattery() ? 'true' : 'false' }}, fuel_type: "{{ cv.getFuelType() }}", - mfg_name: "{{ vehicle.getManufacturer().getName() }}", model_year: "{{ cv.getModelYear() }}", plate_number: "{{ cv.getPlateNumber() }}", status_condition: "{{ cv.getStatusCondition() }}", - vehicle: "{{ vehicle.getID() }}", - vehicle_label: "{{ vehicle.getMake() ~ ' (' ~ vehicle.getModelYearStart() ~ ' ' ~ vehicle.getModelYearEnd() ~ ')' }}", - vmfg: "{{ vehicle.getManufacturer().getID() }}", + vehicle: "{{ vehicle ? vehicle.getID() }}", + vehicle_label: "{{ vehicle ? vehicle.getManufacturer().getName() ~ ' ' ~ vehicle.getMake() ~ ' (' ~ vehicle.getModelYearFrom() ~ ' ' ~ vehicle.getModelYearTo() ~ ')' }}", + vmfg: "{{ vehicle ? vehicle.getManufacturer().getID() }}", warranty_code: "{{ cv.getWarrantyCode() }}", warranty_expiration: "{{ cv.getWarrantyExpiration() ? cv.getWarrantyExpiration()|date('d M Y') }}" }; @@ -483,61 +548,45 @@ numberTable.reload(); }); - // add a vehicle to the table - $("#btn-add-vehicle").click(function() { - var id = $("#vehicle").val(); - var index = $("#vehicle").find(":selected").data('index'); - - if (vehicleIds.indexOf(id) !== -1) { - swal({ - title: 'Whoops', - text: 'This vehicle is already on the list.', - type: 'warning' - }); - - return true; - } - - // add vehicle to arrays - vehicleIds.push(id); - vehicleRows.push(mfgVehicles[index]); - - // refresh the data table - vehicleTable.originalDataSet = vehicleRows; - vehicleTable.reload(); - }); - - // remove item from table - $(document).on('click', '.btn-delete', function(e) { + // remove mobile number from table + $(document).on('click', '.btn-delete-number', function(e) { var btn = $(this); var id = $(this).data('id'); var table = $(this).closest('table'); - var rowArray; - var rowIds; - var dt; - if (table.parent().prop('id') == 'data-mobile-numbers') { - rowArray = numberRows; - dt = numberTable; + // remove from ids + numberIds.splice(numberIds.indexOf(id), 1); - // remove from ids - numberIds.splice(numberIds.indexOf(id), 1); - } else { - rowArray = vehicleRows; - dt = vehicleTable; - } - - $.each(rowArray, function(index, row) { + $.each(numberRows, function(index, row) { if (row.id == id) { - delete rowArray.splice(index, 1); + numberRows.splice(index, 1); return false; } }); // reload table - dt.row(btn.parents('tr')).remove(); - dt.originalDataSet = rowArray; - dt.reload(); + numberTable.row(btn.parents('tr')).remove(); + numberTable.originalDataSet = numberRows; + numberTable.reload(); + }); + + // remove vehicle from table + $(document).on('click', '.btn-delete-vehicle', function(e) { + var btn = $(this); + var id = $(this).data('index'); + var table = $(this).closest('table'); + + $.each(vehicleRows, function(index, row) { + if (row.index == id) { + vehicleRows.splice(index, 1); + return false; + } + }); + + // reload table + vehicleTable.row(btn.parents('tr')).remove(); + vehicleTable.originalDataSet = vehicleRows; + vehicleTable.reload(); }); // display create vehicle form @@ -698,7 +747,7 @@ var firstfield = false; // remove all error classes first - removeErrors(); + removeVehicleErrors(); // check for required fields $.each(fields, function(field, value) { @@ -739,8 +788,7 @@ // build table row var row = fields; - row.mfg_name = $("#vmfg option:selected").text(); - row.vehicle_label = $("#vehicle option:selected").text(); + row.vehicle_label = $("#vmfg option:selected").text() + " " + $("#vehicle option:selected").text(); row.flag_active = row.flag_active == 1 ? true : false; // add optional field @@ -778,14 +826,15 @@ $("#flag-motolite-battery").prop('checked', false).change(); $("#flag-active").prop('checked', true); $("#vehicle-form").find("input[type='text'], select").val("").change(); - removeErrors(); + removeVehicleErrors(); }); // remove all error classes on vehicle form - function removeErrors() { - $(".form-control-danger").removeClass('form-control-danger'); - $("[data-field]").removeClass('has-danger'); - $(".form-control-feedback[data-field]").addClass('hide'); + function removeVehicleErrors() { + var form = $("#vehicle-form"); + form.find(".form-control-danger").removeClass('form-control-danger'); + form.find("[data-field]").removeClass('has-danger'); + form.find(".form-control-feedback[data-field]").addClass('hide'); } // mobile numbers data table @@ -804,15 +853,24 @@ columns: [ { field: 'id', - title: 'Mobile Number' + title: 'Mobile Number', + template: function (row, index, datatable) { + return row.id + ''; + } }, { field: 'date_registered', - title: 'Date Registered' + title: 'Date Registered', + template: function (row, index, datatable) { + return row.date_registered + ''; + } }, { field: 'date_confirmed', - title: 'Date Confirmed' + title: 'Date Confirmed', + template: function (row, index, datatable) { + return row.date_confirmed + ''; + } }, { field: 'Actions', @@ -821,7 +879,7 @@ sortable: false, overflow: 'visible', template: function (row, index, datatable) { - return ''; + return ''; }, } ], @@ -845,60 +903,92 @@ }, columns: [ { - field: 'mfg_name', - title: 'Manufacturer' + field: 'name', + title: 'Name', + template: function (row, index, datatable) { + return row.name + ''; + } }, { field: 'vehicle_label', title: 'Vehicle', - width: 150 + width: 150, + template: function (row, index, datatable) { + return row.vehicle_label + ''; + } }, { field: 'model_year', - title: 'Year' + title: 'Year', + template: function (row, index, datatable) { + return row.model_year + ''; + } }, { field: 'plate_number', - title: 'Plate No.' + title: 'Plate No.', + template: function (row, index, datatable) { + return row.plate_number + ''; + } }, { field: 'color', title: 'Color', - width: 70 + width: 70, + template: function (row, index, datatable) { + return row.color + ''; + } }, { field: 'status_condition', - title: 'Cond.' + title: 'Cond.', + template: function (row, index, datatable) { + return row.status_condition + ''; + } }, { field: 'fuel_type', - title: 'Fuel Type' + title: 'Fuel Type', + template: function (row, index, datatable) { + return row.fuel_type + ''; + } }, { field: 'warranty_code', - title: 'Wty. Code' + title: 'Wty. Code', + template: function (row, index, datatable) { + return row.warranty_code + ''; + } }, { field: 'warranty_expiration', title: 'Wty. Exp.', - width: 80 + width: 80, + template: function (row, index, datatable) { + return row.warranty_expiration + ''; + } }, { field: 'battery_label', title: 'Battery', - width: 150 + width: 150, + template: function (row, index, datatable) { + return row.battery_label + ''; + } }, { field: 'flag_active', title: 'Status', template: function (row, index, datatable) { if (row.flag_active === true) { - tag = 'Active'; + html = 'Active'; } else { - tag = 'Inactive'; + html = 'Inactive'; } - return tag; + html += ''; + + return html; } }, { @@ -910,8 +1000,8 @@ template: function (row, index, datatable) { html = ''; - html += '' + - ''; + html += '' + + ''; return html; },