Sistema de gestión de aparcamientos en JavaScript

Oct 20 2020

Hipótesis

Hay un estacionamiento de pago, con las siguientes tarifas: $1 for the first hour and $0,5 por cada hora subsiguiente. La capacidad de estacionamiento es de 10 espacios.

Necesario

Realizar un sistema de gestión de actividades para el Parking con las siguientes características:

  • Cualquier intervalo de estacionamiento se redondea a la hora más cercana.
  • Cuando un automóvil nuevo ingresa al estacionamiento, el sistema de estacionamiento toma el número de registro del automóvil.
  • Cuando un coche sale del aparcamiento, el sistema emite un resumen, en función de la duración de la estancia. ¿Qué información consideras esencial como parte del resumen?
  • Cualquier cliente puede ver la lista de autos estacionados en el estacionamiento en un momento determinado.

Requerimientos técnicos

  • La implementación se realizará mediante JavaScript, todos los datos se almacenan en la memoria, sin necesidad de almacenamiento permanente.
  • Escribe código limpio.
  • Cree una GUI simple que permita ingresar y leer datos desde la computadora; mostrar los datos en la misma página.

Solución

Usé Bootstrap 4 por estética. El script en sí solo usa JavaScript. jQuery se incluye para el correcto funcionamiento de 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>

¿Ve algún problema con esta aplicación, incluso desde el punto de vista de la seguridad ?

¡Gracias!

Respuestas

5 CertainPerformance Oct 21 2020 at 00:50

Prefieroconst sobre lety var. Cuando declaras una variable con let, estás indicando a cualquier lector del código que la variable puede reasignarse en cualquier momento. Cuando la reasignación no es realmente necesaria para que el código funcione, esto da como resultado una sobrecarga cognitiva indeseable; puede ser una preocupación constante en el fondo de la mente "Este nombre de variable puede reasignarse en cualquier momento, entonces, ¿a qué se refiere ahora? puede no ser lo que se asignó originalmente. * Verhttps://softwareengineering.stackexchange.com/questions/278652/how-much-should-i-be-using-let-vs-const-in-es6

Es mejor evitarlo por varcompleto, tiene demasiados problemas para que valga la pena usarlo hoy en día; lo más importante es que tiene un alcance de función poco intuitivo en lugar de un alcance de bloque.

Relleno de cadenas Cuando desee rellenar el inicio de una cadena con algunos caracteres iniciales para hacer que la cadena tenga una longitud particular (por ejemplo, aquí, 9y 10para 09y 10respectivamente), el método más apropiado espadStart

Los literales de plantilla son excelentes cuando necesita interpolar múltiples variables en una cadena; a menudo son preferibles al estilo de ' + someVar + '.

Teniendo en cuenta los 3 consejos anteriores, la formatDatefunción se puede refactorizar para:

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 se puede hacer mucho más conciso y sin reasignar el parámetro:

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

/obligará a los no números a convertirse en números; que no es necesario llamar Numberen dprimer lugar.

renterTableAquí se pueden realizar muchas mejoras. Primero, el nombre de la función no es del todo intuitivo: ¿qué hace algo llamado renterTable(sustantivo)? ¿Es una variable que contiene un HTMLTableElement? ¿Crea una tabla y la devuelve? No, vuelve a renderizar la tabla en el DOM dados los datos en la carsmatriz. Considere llamarlo renderTableo renderRenterTable(verbo) en su lugar.

La construcción de HTML concatenando la entrada del usuario no es segura . Tiene:

  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;

Esto permitirá la ejecución de código arbitrario. Imagínese si alguien dijera:

Intente conectar la siguiente placa de matrícula cuando registre un automóvil, ¡no creerá lo que sucede después!

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

donde el alert("evil")puede ser reemplazado con lo que quiera el phisher. Entonces, algunos usuarios no demasiado inteligentes podrían caer en la trampa y comprometer su información de inicio de sesión / fondos de cuenta / etc.

Esto se puede solucionar asegurándose de que licenseeprimero contenga solo caracteres válidos, por ejemplo, turn:

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

Pero eso sigue siendo solo un trabajo de parche. Si el script se desarrolla en el futuro, temeré no desinfectar accidentalmente una entrada antes de insertarla en el DOM. Cuando la entrada del usuario está involucrada, preferiría evitar la interpolación de cadenas por completo con HTML y, en su lugar, insertarlo textContenten el elemento después de que el elemento se coloque en el documento, por ejemplo:

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

O, mejor aún , para proyectos más grandes, considere usar un marco que permita la interpolación concisa de la entrada en el DOM, como React:

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

Todo lo anterior también se aplica a sumarryTable, que tiene el mismo tipo de vulnurabilidad de HTML concatenado

Ortografía La ortografía es importante en la programación: los nombres de variables escritos correctamente ayudan a prevenir errores. Llámalo en su summaryTablelugar. Además, la palabra "licenciatario" se refiere a la persona que posee una licencia, pero su variable se refiere a la cadena de la placa. Considere llamarlo en licenseIDlugar de licensee; será menos confuso.

EvitarinnerHTML Por razones similares a las anteriores, use solo innerHTMLcuando necesite insertar marcado HTML. Si solo desea insertar texto en un elemento, use textContent; será más seguro, más rápido y no tendrá problemas con los caracteres que tienen un significado especial en HTML. Esta:

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

puede ser

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

(También podría guardar la referencia en en #placesCountlugar de volver a seleccionarla cada vez que se llama a la función)

Evite los controladores en línea Dentro del marcado de filas, tiene:

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

Los manejadores en línea tienen demasiados problemas para que valga la pena usarlos en el código moderno. Requieren contaminación global y tienen una cadena de alcance loca, entre otros problemas. Adjunte el detector de eventos correctamente utilizando JavaScript en su lugar. Adjunte el oyente a cada botón cuando el botón se inserte en el DOM o, posiblemente una mejor opción, use la delegación de eventos. Vea los comentarios a continuación también:

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

Prefiera la notación de puntos cuando sea posible, es más conciso y más fácil de leer: tiene

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

Estos se pueden cambiar a:

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

(recuerde usar siempre en === lugar de == ; ==tiene reglas extrañas que los lectores de un script no deberían tener que entender para comprender la lógica que se implementa)

Sintaxis moderna La sintaxis moderna y los métodos modernos se utilizan en el código fuente, lo cual es bueno: lo convierte en un código limpio, conciso y legible. Para el código de producción, use Babel y polyfills para transpilar automáticamente la fuente a ES5 para que los navegadores obsoletos como IE también puedan ejecutar el código.