System zarządzania parkingami w JavaScript

Oct 20 2020

Hipoteza

Parking płatny, z następującymi stawkami: $1 for the first hour and $0,5 za każdą kolejną godzinę. Pojemność parkingu wynosi 10 miejsc.

wymagany

Stwórz system zarządzania aktywnością na parkingu o następujących cechach:

  • Każdy okres parkowania jest zaokrąglany w górę do najbliższej godziny.
  • Gdy nowy samochód wjeżdża na parking, system parkowania przyjmuje jego numer rejestracyjny.
  • Kiedy samochód wyjeżdża z parkingu, system wystawia podsumowanie w zależności od długości pobytu. Jakie informacje uważasz za istotne jako część podsumowania?
  • Każdy klient może zobaczyć listę samochodów zaparkowanych na parkingu w danym momencie.

Wymagania techniczne

  • Wdrożenie zostanie wykonane przy użyciu JavaScript, wszystkie dane będą przechowywane w pamięci, bez konieczności przechowywania ich na stałe.
  • Napisz czysty kod.
  • Stwórz prosty GUI, który umożliwia wprowadzanie i odczytywanie danych z komputera; wyświetlić dane na tej samej stronie.

Rozwiązanie

Użyłem Bootstrap 4 ze względu na estetykę. Sam skrypt używa tylko JavaScript. jQuery jest dołączony do podstawowego działania 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>

Czy widzisz jakieś problemy z tą aplikacją, w tym z punktu widzenia bezpieczeństwa ?

Dzięki!

Odpowiedzi

5 CertainPerformance Oct 21 2020 at 00:50

Wolęconst ponad leti var. Deklarując zmienną za pomocą let, wskazujesz dowolnemu czytelnikowi kodu, że zmienna może zostać ponownie przypisana w dowolnym momencie. Gdy ponowne przypisanie nie jest w rzeczywistości konieczne, aby kod działał, powoduje to niepożądane narzuty poznawcze - może to być ciągłe zmartwienie z tyłu głowy „Ta nazwa zmiennej może zostać ponownie przypisana w dowolnym momencie, więc do czego się teraz odnosi może nie być tym, do czego został pierwotnie przypisany. * Zobaczhttps://softwareengineering.stackexchange.com/questions/278652/how-much-should-i-be-using-let-vs-const-in-es6

Najlepiej varcałkowicie tego uniknąć , ma zbyt wiele problemów, aby warto było z nich korzystać w dzisiejszych czasach - co najważniejsze, ma nieintuicyjny zakres funkcji zamiast zakresu blokowego.

Wyściółka String Gdy chcesz pad początek napisu z kilkoma czołowymi postaciami zmieniając String szczególną długość (na przykład tutaj, 9i 10na 09i 10odpowiednio), najwłaściwszą metodą jest do użytkupadStart

Literały szablonów są świetne, gdy trzeba interpolować wiele zmiennych do ciągu znaków - często są lepsze niż styl ' + someVar + '.

Biorąc pod uwagę powyższe 3 wskazówki, formatDatefunkcję można refaktoryzować na:

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}`;
}

Sekundy do godzin można uczynić znacznie bardziej zwięzłym i bez ponownego przypisywania parametru:

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

/wymusi nieliczby na liczbach; nie trzeba zadzwonić Numberna dpierwszy.

renterTableMożna tu wprowadzić wiele ulepszeń. Po pierwsze, nazwa funkcji nie jest całkowicie intuicyjna - co robi coś o nazwie renterTable(rzeczownik)? Czy jest to zmienna zawierająca element HTMLTableElement? Czy tworzy tabelę i zwraca ją? Nie, ponownie renderuje tabelę w modelu DOM, biorąc pod uwagę dane w carstablicy. Zamiast tego rozważ nazwanie go renderTablelub renderRenterTable(czasownik).

Tworzenie kodu HTML przez konkatenację danych wejściowych użytkownika jest niebezpieczne. Masz:

  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;

Umożliwi to wykonanie dowolnego kodu. Wyobraź sobie, że ktoś powiedziałby:

Spróbuj podczas rejestracji samochodu podłączyć następującą tablicę rejestracyjną, nie uwierzysz, co będzie dalej!

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

gdzie alert("evil")można go zastąpić czymkolwiek zechce phisher. Wtedy niektórzy niezbyt inteligentni użytkownicy mogą się na to nabrać i ich dane logowania / środki konta / itp. Zostaną naruszone.

Można to naprawić, upewniając się, że najpierw licenseezawiera tylko prawidłowe znaki, np. Turn:

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

Ale to wciąż tylko łatka. Jeśli skrypt zostanie opracowany w przyszłości, bałbym się przypadkowo nie wyczyścić wejścia przed wstawieniem go do DOM. Gdy chodzi o dane wejściowe użytkownika, wolałbym całkowicie uniknąć interpolacji ciągów za pomocą HTML i zamiast tego wstawić textContentelement do elementu po umieszczeniu elementu w dokumencie, np .:

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;
// ...

Lub, jeszcze lepiej , w przypadku większych projektów rozważ użycie frameworka, który pozwala na zwięzłą interpolację danych wejściowych do DOM, takiego jak React:

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

Wszystkie powyższe dotyczą również elementu sumarryTable, który ma ten sam rodzaj wulgarności konkatenowanego kodu HTML

Pisownia Pisownia ma znaczenie w programowaniu - poprawnie napisane nazwy zmiennych pomagają zapobiegać błędom. Nazwij to summaryTablezamiast tego. Ponadto słowo „licencjobiorca” odnosi się do osoby, która posiada licencję, ale Twoja zmienna odnosi się do ciągu znaków rejestracyjnych. Rozważ nazwanie go licenseIDzamiast licensee; będzie mniej zagmatwane.

UnikajinnerHTML Z powodów podobnych do powyższych, używaj tylko innerHTMLwtedy, gdy musisz wstawić znaczniki HTML. Jeśli chcesz tylko wstawić tekst do elementu, użyj textContent; będzie bezpieczniejszy, szybszy i nie będzie miał problemów ze znakami o specjalnym znaczeniu w HTML. To:

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

może być

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

(Można również zapisać odniesienie do #placesCountzamiast ponownego wybierania go przy każdym wywołaniu funkcji)

Unikaj procedur obsługi w wierszu Wewnątrz znaczników wiersza masz:

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

Procedury obsługi wbudowane mają zbyt wiele problemów, aby warto było ich używać w nowoczesnym kodzie. Wymagają między innymi globalnego zanieczyszczenia i mają szalony łańcuch zasięgu. Zamiast tego dołącz poprawnie detektor zdarzeń, używając JavaScript. Dołącz detektor do każdego przycisku, gdy przycisk zostanie wstawiony do DOM, lub, być może lepsza opcja, użyj delegowania zdarzenia. Zobacz również komentarze poniżej:

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);
});

Jeśli to możliwe, wolisz notację kropkową , jest bardziej zwięzła i łatwiejsza do odczytania - masz

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

Można je zmienić na:

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

(pamiętaj, aby zawsze używać === zamiast == ; ==ma dziwne zasady, których czytelnicy skryptu nie powinni musieć rozumieć, aby zrozumieć wdrażaną logikę)

Nowoczesna składnia W kodzie źródłowym wykorzystano nowoczesną składnię i nowoczesne metody, co jest dobre - daje to czysty, zwięzły, czytelny kod. W przypadku kodu produkcyjnego użyj Babel i polyfillów, aby automatycznie przetransponować źródło do ES5, aby przestarzałe przeglądarki, takie jak IE, również mogły uruchamiać kod.