Система управления парковками на JavaScript

Oct 20 2020

Гипотеза

Есть платная автостоянка по следующим тарифам: $1 for the first hour and $0,5 за каждый последующий час. Парковка на 10 мест.

необходимые

Сделайте систему управления деятельностью для парковки со следующими характеристиками:

  • Время парковки округляется до ближайшего часа.
  • Когда новый автомобиль заезжает на стоянку, система парковки принимает регистрационный номер автомобиля.
  • Когда машина выезжает со стоянки, система выдает сводку в зависимости от продолжительности стоянки. Какую информацию вы считаете важной в резюме?
  • Список автомобилей, припаркованных на стоянке в данный момент, может увидеть любой покупатель.

Технические требования

  • Реализация будет выполняться с использованием JavaScript, все данные будут храниться в памяти без необходимости постоянного хранения.
  • Пишите чистый код.
  • Создать простой графический интерфейс, позволяющий вводить и считывать данные с компьютера; отображать данные на той же странице.

Решение

Я использовал Bootstrap 4 для эстетики. Сам скрипт использует только JavaScript. jQuery включен для нормальной работы Bootstrap.

let cars = [];
const addCarButton = document.querySelector('#carButton');
const minLicenseeLength = 7;
const payPerHour = 0.5;
const payFirstHour = 1;
const totalPlaces = 10;

const formatDate = (date) => {
  var hours = date.getHours();
  var minutes = date.getMinutes();
  var ampm = hours >= 12 ? 'PM' : 'AM';
  hours = hours % 12;
  hours = hours ? hours : 12;
  minutes = minutes < 10 ? '0' + minutes : minutes;
  var strTime = hours + ':' + minutes + ' ' + ampm;
  return date.getDate() + "/" + (date.getMonth() + 1) + "/" + date.getFullYear() + " " + strTime;
}

const secondsToHours = (d) => {
  d = Number(d);
  let h = Math.ceil(d / 3600);
  return h;
}

const renterTable = () => {
  let results = '';
  for (var i = 0; i < cars.length; i++) {
    let licensee = cars[i].licensee;
    let arrival = formatDate(cars[i].arrival);
    let leave = cars[i].leave === '-' ? '-' : formatDate(cars[i].leave);

    results += `<tr>
          <td>${licensee}</td> <td>${arrival}</td>
          <td>${leave}</td> <td>${showStatus(cars[i])}</td>
          <td class="text-right">${makeBill(cars[i])}</td> <td class="text-right"> <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                    </td>
        </tr>`;
  }
  document.querySelector("#parking tbody").innerHTML = results;
}

const showStatus = (car) => {
  return car.isParked ? "Parked" : "Has left";
}

const changeStatus = (event) => {
  cars[event.target.dataset.row].isParked = false;
}

const setLeaveTime = (event) => {
  cars[event.target.dataset.row].leave = new Date(Date.now());
}

const countAvailablePlaces = (event) => {
  document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;
}

const setClassForBadge = () => {
  let badgeClassName = cars.length == totalPlaces ? 'badge badge-danger' : 'badge badge-success';
  document.querySelector('#placesCount').setAttribute('class', badgeClassName);
}

const calculateHoursBilled = (car) => {
  let arrivedAt = new Date(car.arrival).getTime();
  let leftAt = new Date(car.leave).getTime();
  return secondsToHours((leftAt - arrivedAt) / 1000); //duration in seconds
}

const makeBill = (car) => {
  let hoursBilled = calculateHoursBilled(car);
  let billValue = car.isParked ? "-" : "$" + (payFirstHour + (hoursBilled - 1) * payPerHour); return billValue; } const printSummary = (event) => { let car = cars[event.target.dataset.row]; let sumarryTable = `<table class="table table-bordered m-0"> <tr> <td class="font-weight-bold">Registration number</td> <td>${car.licensee}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Arrival</td>
          <td>${formatDate(car.arrival)}</td> </tr> <tr> <td class="font-weight-bold">Departure</td> <td>${formatDate(car.leave)}</td>
        </tr>
        <tr>
          <td class="font-weight-bold">Billable hours</td>
          <td>${calculateHoursBilled(car)}</td> </tr> <tr> <td class="font-weight-bold">Bill value</td> <td>${makeBill(car)}</td>
      </tr></table>`;

  document.querySelector('#modalBody').innerHTML = sumarryTable;
}

const showSummary = (event) => {
  changeStatus(event);
  setLeaveTime(event);
  renterTable();
  printSummary(event);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(event);
  }, 3000);
}

const addCar = () => {
  let newLicensee = document.querySelector("#carValue").value;

  let newCar = {
    licensee: newLicensee,
    arrival: new Date(),
    leave: '-',
    isParked: true
  }

  // Add new car to the cars array

  document.querySelector('#message').style.display = 'none';
  if (newLicensee.length >= minLicenseeLength && cars.length < totalPlaces) {
    cars.unshift(newCar);
  } else {
    if (newLicensee.length < minLicenseeLength) {
      document.querySelector('#message').style.display = 'block';
    }
  }

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled', true);
  }

  setClassForBadge();

  //Update places count
  countAvailablePlaces(event);

  // Empty text box
  document.querySelector("#carValue").value = '';

  // Render the table
  renterTable();
}

const freeSpot = (event) => {
  cars.splice(event.target.dataset.row, 1);
  setClassForBadge();

  if (cars.length == totalPlaces) {
    document.querySelector('#carButton').setAttribute('disabled');
  } else {
    document.querySelector('#carButton').removeAttribute('disabled');
  }

  // Render Table again after delete 
  renterTable();
  //Update places count
  countAvailablePlaces(event);
}

// Add new car to the array
addCarButton.addEventListener('click', addCar);

// Render Table
renterTable();

//Show places count at page load
countAvailablePlaces(event);
#addForm {
  position: relative;
}

#message {
  display: none;
  position: relative;
  font-size: 10px;
  position: absolute;
}

#placesCount {
  font-size: 90%;
}

#parking th,
#parking td {
  white-space: nowrap;
  font-size: 14px;
}

#myModal .font-weight-bold {
  font-weight: 500 !important;
}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

<div class="container">
  <div class="card my-3">
    <div class="card-header px-3 d-flex">
      <h5 class="m-0">Parking management</h5>
      <div class="ml-auto">
        <span id="placesCount" class="badge badge-success"></span> available places
      </div>
    </div>
    <div class="card-body p-0">
      <div class="input-group p-2" id="addForm">
        <input type="text" class="form-control" id="carValue" placeholder="Registration number">
        <div class="input-group-append">
          <button id="carButton" class="btn btn-sm btn-success">Park car</button>
        </div>
        <p id="message" class="text-danger m-0">Registration number invalid</p>
      </div>
      <div class="table-responsive">
        <table id="parking" class="table table-striped m-0">
          <thead>
            <tr>
              <th>Registration no</th>
              <th>Arrival</th>
              <th>Departure</th>
              <th>Status</th>
              <th class="text-right">Bill</th>
              <th class="text-right">Actions</th>
            </tr>
          </thead>
          <tbody></tbody>
        </table>
      </div>
    </div>
  </div>
</div>

<div class="modal" id="myModal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h6 class="modal-title">Sumar Parcare</h6>
        <button type="button" class="close" data-dismiss="modal">&times;</button>
      </div>
      <div id="modalBody" class="modal-body"></div>
    </div>
  </div>
</div>

Вы видите какие-либо проблемы с этим приложением, в том числе с точки зрения безопасности ?

Благодаря!

Ответы

5 CertainPerformance Oct 21 2020 at 00:50

Предпочитаюconst более letи var. Когда вы объявляете переменную с помощью let, вы указываете любому читателю кода, что переменную можно переназначить в любое время. Когда переназначение на самом деле не требуется для работы кода, это приводит к нежелательным когнитивным накладным расходам - ​​это может быть постоянное беспокойство в глубине души "Это имя переменной может быть переназначено в любое время, так что к чему оно сейчас относится может быть не тем, для чего он был изначально назначен. * См.https://softwareengineering.stackexchange.com/questions/278652/how-much-should-i-be-using-let-vs-const-in-es6

Лучше всего избегать varполностью, у него слишком много проблем, чтобы его стоило использовать в настоящее время - что наиболее важно, он имеет неинтуитивную область действия вместо области действия блока.

Заполнение строки Если вы хотите дополнить начало строки некоторыми ведущими символами, чтобы сделать строку определенной длины (например, здесь 9и 10до 09и 10соответственно), наиболее подходящий метод для использования -padStart

Шаблонные литералы удобны , когда вам нужно интерполировать несколько переменных в строку - они часто предпочтительнее стиля ' + someVar + '.

Принимая во внимание 3 приведенных выше совета, formatDateфункцию можно изменить так, чтобы она:

const formatDate = (date) => {
  const hoursMilitary = date.getHours();
  const minutesToDisplay = String(date.getMinutes()).padStart('0', 2);
  const ampm = hoursMilitary >= 12 ? 'PM' : 'AM';
  const hoursToDisplay = (hoursMilitary % 12) || 12;
  const strTime = `${hoursToDisplay}:${minutesToDisplay} ${ampm}`; return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${strTime}`;
}

secondsToHours можно сделать гораздо лаконичнее и без переназначения параметра:

const secondsToHours = d => Math.ceil(d / 3600);

/будет принуждать не числа к числам; Вам не нужно звонить Numberпо dпервому.

renterTableЗдесь можно сделать множество улучшений. Во-первых, название функции не совсем интуитивно понятно - что делает то, что называется renterTable(существительное)? Это переменная, содержащая HTMLTableElement? Создает ли он таблицу и возвращает ее? Нет, он повторно отображает таблицу в DOM с учетом данных в carsмассиве. Попробуйте вместо этого назвать его renderTableили renderRenterTable(глагол).

Создание HTML путем объединения вводимых пользователем данных небезопасно. У вас есть:

  results += `<tr>
        <td>${licensee}</td>
        <td>${arrival}</td> <td>${leave}</td>
        <td>${showStatus(cars[i])}</td> <td class="text-right">${makeBill(cars[i])}</td>
        <td class="text-right">
          <button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>
                  </td>
      </tr>`;
}
document.querySelector("#parking tbody").innerHTML = results;

Это позволит выполнять произвольный код. Представьте, если бы кто-то сказал:

Попробуйте подключить следующий номерной знак при регистрации автомобиля, вы не поверите, что будет дальше!

<img src onerror='alert("evil")'>

где alert("evil")можно заменить на все, что захочет фишер. Тогда некоторые не слишком умные пользователи могут попасться на это и скомпрометировать их данные для входа / средства учетной записи и т. Д.

Это можно исправить, убедившись, что licenseeсначала содержат только допустимые символы, например, turn:

let newLicensee = document.querySelector("#carValue").value;
if (!/^[a-z\d]+$/.test(newLicensee)) {
  // display error message to user: license invalid
  return;
}

Но это еще только патч. Если сценарий будет разработан в будущем, я бы побоялся случайно не очистить ввод перед вставкой его в DOM. Когда задействован пользовательский ввод, я бы предпочел полностью избегать интерполяции строк с помощью HTML и вместо этого вставлять textContentв элемент после того, как элемент помещен в документ, например:

const tr = document.querySelector("#parking tbody").appendChild(document.createElement('tr'));
tr.innerHTML = `
  <td></td>
  <td></td>
  <td></td>
  ...
`;
tr.children[0].textContent = licensee;
tr.children[1].textContent = arrival;
tr.children[2].textContent = leave;
// ...

Или, что еще лучше , для более крупных проектов рассмотрите возможность использования фреймворка, который позволяет краткую интерполяцию ввода в DOM, например React:

const CarRow = (carInfo) => (
  <tr>
    <td>{carInfo.licensee}</td>
    <td>{carInfo.arrival}</td>
    <td>{carInfo.leave}</td>
    ...

Все вышеперечисленное также относится к sumarryTableтегу, который имеет такую ​​же уязвимость конкатенированного HTML

Орфография Орфография имеет значение в программировании - правильно написанные имена переменных помогают предотвратить ошибки. Назовите это summaryTableвместо этого. Кроме того, слово «лицензиат» относится к лицу, имеющему лицензию, но ваша переменная относится к строке номерного знака. Попробуйте назвать это licenseIDвместо licensee; это будет менее запутанно.

ИзбегайтеinnerHTML По причинам, аналогичным указанным выше, используйте только innerHTMLтогда, когда вам нужно вставить разметку HTML. Если вы просто хотите вставить текст в элемент, используйте textContent; это будет безопаснее, быстрее и не будет проблем с символами, имеющими особое значение в HTML. Этот:

document.querySelector('#placesCount').innerHTML = totalPlaces - cars.length;

возможно

document.querySelector('#placesCount').textContent = totalPlaces - cars.length;

(Можно также сохранить ссылку #placesCountвместо того, чтобы повторно выбирать ее каждый раз при вызове функции)

Избегайте встроенных обработчиков. Внутри разметки строки у вас есть:

<button data-row="${i}" onclick="showSummary(event)" data-toggle="modal" data-target="#myModal" class="btn btn-sm btn-success">Summary</button>

У встроенных обработчиков слишком много проблем, чтобы их можно было использовать в современном коде. Они требуют глобального загрязнения и, помимо прочего, имеют сумасшедшую цепочку масштабов. Правильно подключите прослушиватель событий, используя вместо этого JavaScript. Либо прикрепите слушателя к каждой кнопке, когда кнопка вставляется в DOM, либо, возможно, лучше, используйте делегирование событий. Смотрите также комментарии ниже:

table.addEventListener('click', (event) => {
  if (!event.target.matches('button')) {
    return;
  }
  // I renamed this from "row" to "rowIndex" to make the variable name more precise;
  // it's not an actual row, it's only an index
  const { rowIndex } = event.target.dataset;
  // Now that the rowIndex has been extracted, grab the car,
  // then pass the car along instead of extracting it inside each function:
  const car = cars[rowIndex];
  changeStatus(car);
  setLeaveTime(car);
  
  renderRenterTable();
  printSummary(car);

  //Free the parking place, 3 seconds after the summary is released
  setTimeout(function() {
    freeSpot(rowIndex);
  }, 3000);
});

По возможности предпочитайте точечную нотацию , она более лаконична и удобна для чтения - у вас есть

document.querySelector('#placesCount').setAttribute('class', badgeClassName);
// ...
if (cars.length == totalPlaces) {
  document.querySelector('#carButton').setAttribute('disabled');
} else {
  document.querySelector('#carButton').removeAttribute('disabled');
}

Их можно изменить на:

document.querySelector('#placesCount').className = badgeClassName;
// ...
document.querySelector('#carButton').disabled = cars.length === totalPlaces;

(не забудьте всегда использовать === вместо == ; ==есть странные правила, которые читатели сценария не должны понимать, чтобы понять реализуемую логику)

Современный синтаксис В исходном коде используется современный синтаксис и современные методы, и это хорошо - он делает код чистым, лаконичным и читаемым. Для производственного кода используйте Babel и polyfills, чтобы автоматически перенести исходный код на ES5, чтобы устаревшие браузеры, такие как IE, также могли запускать код.