23. Compras sencillas

Versión para imprimir.

A. Introducción

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvcompras?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

G. Archivos

Haz clic en los triángulos para expandir las carpetas

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Productos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvProductoConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Productos</h1>
22
23 <p><a href="carrito.html">Ver carrito</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14 <script type="module" src="lib/js/muestraError.js"></script>
15
16 <script>
17 // Obtiene los parámetros de la página.
18 const params = new URL(location.href).searchParams
19 </script>
20
21</head>
22
23<body onload="if (params.size > 0) {
24 invocaServicio('srv/srvProductoBusca.php?' + params)
25 .then(producto => muestraObjeto(document, producto.body))
26 .catch(muestraError)
27 }">
28
29 <form onsubmit="submitForm('srv/srvDetalleDeVentaAgrega.php', event)
30 .then(modelo => location.href = 'index.html')
31 .catch(muestraError)">
32
33 <h1>Agregar</h1>
34
35 <p><a href="index.html">Cancelar</a></p>
36
37 <input type="hidden" name="id">
38
39 <p>
40 <label>
41 Producto
42 <output name="producto">
43 <progress max="100">Cargando…</progress>
44 </output>
45 </label>
46 </p>
47
48 <p>
49 <label>
50 Precio
51 <output name="precio">
52 <progress max="100">Cargando…</progress>
53 </output>
54 </label>
55 </p>
56
57 <p>
58 <label>
59 Cantidad *
60 <input name="cantidad" type="number" min="0" step="0.01">
61 </label>
62 </p>
63
64 <p>* Obligatorio</p>
65
66 <p><button type="submit">Agregar</button></p>
67
68 </form>
69
70</body>
71
72</html>

J. carrito.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Carrito</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraError.js"></script>
13 <script type="module" src="lib/js/muestraObjeto.js"></script>
14
15</head>
16
17<body onload="invocaServicio('srv/srvVentaEnCapturaBusca.php')
18 .then(venta => muestraObjeto(document, venta.body))
19 .catch(muestraError)">
20
21 <h1>Carrito</h1>
22
23 <p>
24
25 <a href="index.html">Productos</a>
26
27 <button type='button' onclick="if (confirm('Confirma procesar')) {
28 invocaServicio('srv/srvVentaEnCapturaProcesa.php')
29 .then(() => location.href = 'index.html')
30 .catch(muestraError)
31 }">
32 Procesar compra
33 </button>
34
35 </p>
36
37 <p>
38 <label>
39 Folio
40 <output id="folio">
41 <progress max="100">Cargando…</progress>
42 </output>
43 </label>
44 </p>
45
46 <fieldset>
47
48 <legend>Detalle</legend>
49
50 <dl id="detalles">
51 <dt>Cargando…</dt>
52 <dd><progress max="100">Cargando…</progress></dd>
53 </dl>
54
55 </fieldset>
56
57</body>
58
59</html>

K. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16
17 <script>
18 // Obtiene los parámetros de la página.
19 const params = new URL(location.href).searchParams
20 </script>
21
22</head>
23
24<body onload="if (params.size > 0) {
25 invocaServicio('srv/srvDetalleDeVentaBusca.php?' + params)
26 .then(modelo => muestraObjeto(document, modelo.body))
27 .catch(muestraError)
28 }">
29
30 <form onsubmit="submitForm('srv/srvDetalleDeVentaModifica.php', event)
31 .then(modelo => location.href = 'carrito.html')
32 .catch(muestraError)">
33
34 <h1>Modificar</h1>
35
36 <p><a href="carrito.html">Cancelar</a></p>
37
38 <input type="hidden" name="prodId">
39
40 <p>
41 <label>
42 Producto
43 <output name="prodNombre">
44 <progress max="100">Cargando…</progress>
45 </output>
46 </label>
47 </p>
48
49 <p>
50 <label>
51 Precio
52 <output name="precio">
53 <progress max="100">Cargando…</progress>
54 </output>
55 </label>
56 </p>
57
58 <p>
59 <label>
60 Cantidad *
61 <input name="cantidad" type="number" min="0" step="0.01">
62 </label>
63 </p>
64
65 <p>* Obligatorio</p>
66
67 <p>
68
69 <button type="submit">Guardar</button>
70
71 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
72 invocaServicio('srv/srvDetalleDeVentaElimina.php?' + params)
73 .then(() => location.href = 'carrito.html')
74 .catch(muestraError)
75 }">
76 Eliminar
77 </button>
78
79 </p>
80
81 </form>
82
83</body>
84
85</html>

L. Carpeta « srv »

Versión para imprimir.

A. Carpeta « srv / modelo »

1. srv / modelo / DetalleDeVenta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Venta.php";
5require_once __DIR__ . "/Producto.php";
6
7class DetalleDeVenta
8{
9
10 public ?Venta $venta;
11 public ?Producto $producto;
12 public float $cantidad;
13 public float $precio;
14
15 public function __construct(
16 float $cantidad = NAN,
17 float $precio = NAN,
18 ?Producto $producto = null,
19 ?Venta $venta = null
20 ) {
21 $this->cantidad = $cantidad;
22 $this->precio = $precio;
23 $this->producto = $producto;
24 $this->venta = $venta;
25 }
26
27 public function valida()
28 {
29 if ($this->producto === null)
30 throw new Exception("Detalle de venta sin producto.");
31 if (is_nan($this->cantidad))
32 throw new ProblemDetails(
33 status: ProblemDetails::BadRequest,
34 type: "/error/cantidadincorrecta.html",
35 title: "La cantidad no puede ser NAN.",
36 );
37 if (is_nan($this->precio))
38 throw new ProblemDetails(
39 status: ProblemDetails::BadRequest,
40 type: "/error/precioincorrecto.html",
41 title: "El precio no puede ser NAN.",
42 );
43 }
44}
45

2. srv / modelo / Producto.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../../lib/php/validaNombre.php";
5
6class Producto
7{
8
9 public int $id;
10 public string $nombre;
11 public float $existencias;
12 public float $precio;
13
14 public function __construct(
15 string $nombre = "",
16 float $existencias = NAN,
17 float $precio = NAN,
18 int $id = 0
19 ) {
20 $this->id = $id;
21 $this->nombre = $nombre;
22 $this->existencias = $existencias;
23 $this->precio = $precio;
24 }
25
26 public function valida()
27 {
28 validaNombre($this->nombre);
29 if (is_nan($this->existencias))
30 throw new ProblemDetails(
31 status: ProblemDetails::BadRequest,
32 type: "/error/existenciasincorrectas.html",
33 title: "Las existencias no pueden ser NAN.",
34 );
35 if (is_nan($this->precio))
36 throw new ProblemDetails(
37 status: ProblemDetails::BadRequest,
38 type: "/error/precioincorrecto.html",
39 title: "El precio no puede ser NAN.",
40 );
41 }
42}
43

3. srv / modelo / Venta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/DetalleDeVenta.php";
5
6class Venta
7{
8
9 public int $id;
10 public bool $enCaptura;
11 /** @var DetalleDeVenta[] */
12 public array $detalles;
13
14 public function __construct(
15 bool $enCaptura = false,
16 array $detalles = [],
17 int $id = 0
18 ) {
19 $this->id = $id;
20 $this->enCaptura = $enCaptura;
21 $this->detalles = $detalles;
22 }
23
24 public function valida()
25 {
26 foreach ($this->detalles as $detalle) {
27 if (!($detalle instanceof DetalleDeVenta))
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/detalledeventaincorrecto.html",
31 title: "Tipo incorrecto para un etalle de venta.",
32 );
33 $detalle->valida();
34 }
35 }
36}
37

B. srv / srvDetalleDeVentaAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeDecimal.php";
7require_once __DIR__ . "/../lib/php/pdFaltaId.php";
8require_once __DIR__ . "/modelo/Producto.php";
9require_once __DIR__ . "/modelo/DetalleDeVenta.php";
10require_once __DIR__ . "/bd/detalleDeVentaAgrega.php";
11
12ejecutaServicio(function () {
13 $id = leeEntero("id");
14 if ($id === null) throw pdFaltaId();
15 $cantidad = leeDecimal("cantidad");
16 if ($cantidad === null)
17 throw new ProblemDetails(
18 status: ProblemDetails::BadRequest,
19 type: "/error/faltacantidad.html",
20 title: "Falta la cantidad."
21 );
22 $producto = new Producto(id: $id);
23 $modelo = new DetalleDeVenta(producto: $producto, cantidad: $cantidad);
24 detalleDeVentaAgrega($modelo);
25 $producto = $modelo->producto;
26 $id = htmlentities($producto->id);
27 return JsonResponse::created("/srv/srvDetalleDeVentaBusca.php?id=$id", [
28 "prodId" => ["value" => $producto->id],
29 "prodNombre" => ["value" => $producto->nombre],
30 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
31 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
32 ]);
33});
34

C. srv / srvDetalleDeVentaBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/detalleDeVentaBusca.php";
8
9ejecutaServicio(function () {
10 $prodId = leeEntero("prodId");
11 if ($prodId === null) throw pdFaltaId();
12 $modelo = detalleDeVentaBusca($prodId);
13 if ($modelo === false) {
14 $htmlId = htmlentities($prodId);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/detalledeventanoencontrado.html",
18 title: "Detalle de venta no encontrado.",
19 detail: "No se encontró ningún detalle de venta con el id de producto "
20 . $htmlId . ".",
21 );
22 }
23 $producto = $modelo->producto;
24 return [
25 "prodId" => ["value" => $producto->id],
26 "prodNombre" => ["value" => $producto->nombre],
27 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
28 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
29 ];
30});
31

D. srv / srvDetalleDeVentaElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/detalleDeVentaElimina.php";
8
9ejecutaServicio(function () {
10 $prodId = leeEntero("prodId");
11 if ($prodId === null) throw pdFaltaId();
12 detalleDeVentaElimina($prodId);
13 return JsonResponse::noContent();
14});
15

E. srv / srvDetalleDeVentaModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeDecimal.php";
7require_once __DIR__ . "/modelo/Producto.php";
8require_once __DIR__ . "/modelo/DetalleDeVenta.php";
9require_once __DIR__ . "/bd/detalleDeVentaModifica.php";
10
11ejecutaServicio(function () {
12 $prodId = leeEntero("prodId");
13 if ($prodId === null) throw pdFaltaId();
14 $producto = new Producto(id: $prodId);
15 $cantidad = leeDecimal("cantidad");
16 if ($cantidad === null)
17 throw new ProblemDetails(
18 status: ProblemDetails::BadRequest,
19 type: "/error/faltacantidad.html",
20 title: "Falta la cantidad."
21 );
22 $modelo = new DetalleDeVenta(producto: $producto, cantidad: $cantidad);
23 detalleDeVentaModifica($modelo);
24 $producto = $modelo->producto;
25 return [
26 "prodId" => ["value" => $producto->id],
27 "prodNombre" => ["value" => $producto->nombre],
28 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
29 "cantidad" => ["valueAsNumber" => $modelo->cantidad],
30 ];
31});
32

F. srv / srvProductoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = productoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::BadRequest,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 } else {
22 return [
23 "id" => ["value" => $modelo->id],
24 "producto" => ["value" => $modelo->nombre],
25 "precio" => ["value" => "$" . number_format($modelo->precio, 2)],
26 ];
27 }
28});
29

G. srv / srvProductoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/productoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = productoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $nombre = htmlentities($modelo->nombre);
12 $precio = htmlentities("$" . number_format($modelo->precio, 2));
13 $existencias = htmlentities(number_format($modelo->existencias, 2));
14 $render .=
15 "<dt>$nombre</dt>
16 <dd>
17 <a href='agrega.html?id=$id'>Agregar al carrito</a>
18 </dd>
19 <dd>
20 <dl>
21 <dt>Precio</dt>
22 <dd>$precio</dd>
23 <dt>Existencias</dt>
24 <dd>$existencias</dd>
25 </dl>
26 </dd>";
27 }
28 return ["lista" => ["innerHTML" => $render]];
29});
30

H. srv / srvVentaEnCapturaBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/modelo/DetalleDeVenta.php";
6require_once __DIR__ . "/bd/ventaEnCapturaBusca.php";
7
8ejecutaServicio(function () {
9 $modelo = ventaEnCapturaBusca();
10 if ($modelo === false)
11 throw new ProblemDetails(
12 status: ProblemDetails::BadRequest,
13 type: "/error/ventaencapturanoencontrada.html",
14 title: "Venta en captura no encontrada.",
15 detail: "No se encontró ninguna venta en captura.",
16 );
17 $detalles = $modelo->detalles;
18 $productoIds = [];
19 foreach ($detalles as $detalle) {
20 $id = $detalle->producto->id;
21 $productoIds[$id] = true;
22 }
23 $renderDetalles = "";
24 foreach ($detalles as $detalle) {
25 $producto = $detalle->producto;
26 $prodId = htmlentities($producto->id);
27 $prodNombre = htmlentities($producto->nombre);
28 $precio = htmlentities("$" . number_format($detalle->precio, 2));
29 $cantidad = htmlentities(number_format($detalle->cantidad, 2));
30 $renderDetalles .=
31 "<dt>$prodNombre</dt>
32 <dd>
33 <a href= 'modifica.html?prodId=$prodId'>Modificar o eliminar</a>
34 </dd>
35 <dd>
36 <dl>
37 <dt>Cantidad</dt>
38 <dd>$cantidad</dd>
39 <dt>Precio</dt>
40 <dd>$precio</dd>
41 </dl>
42 </dd>";
43 }
44 $modelo->detalles = [];
45 return [
46 "folio" => ["value" => $modelo->id],
47 "detalles" => ["innerHTML" => $renderDetalles]
48 ];
49});
50

I. srv / srvVentaEnCapturaProcesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/ventaEnCapturaProcesa.php";
5
6ejecutaServicio(function () {
7 ventaEnCapturaProcesa();
8 return JsonResponse::created("/srv/srvVentaEnCapturaBusca.php", []);
9});
10

J. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS VENTA (
7 VENT_ID INTEGER,
8 VENT_EN_CAPTURA INTEGER NOT NULL,
9 CONSTRAINT VENT_PK
10 PRIMARY KEY(VENT_ID)
11 )'
12 );
13 $con->exec(
14 'CREATE TABLE IF NOT EXISTS PRODUCTO (
15 PROD_ID INTEGER,
16 PROD_NOMBRE TEXT NOT NULL,
17 PROD_EXISTENCIAS REAL NOT NULL,
18 PROD_PRECIO REAL NOT NULL,
19 CONSTRAINT PROD_PK
20 PRIMARY KEY(PROD_ID),
21 CONSTRAINT PROD_NOM_UNQ
22 UNIQUE(PROD_NOMBRE)
23 )'
24 );
25 $con->exec(
26 'CREATE TABLE IF NOT EXISTS DET_VENTA (
27 VENT_ID INTEGER NOT NULL,
28 PROD_ID INTEGER NOT NULL,
29 DTV_CANTIDAD REAL NOT NULL,
30 DTV_PRECIO REAL NOT NULL,
31 CONSTRAINT DTV_PK
32 PRIMARY KEY (VENT_ID, PROD_ID),
33 CONSTRAINT DTV_VENT_FK
34 FOREIGN KEY (VENT_ID) REFERENCES VENTA(VENT_ID),
35 CONSTRAINT DTV_PROD_FK
36 FOREIGN KEY (PROD_ID) REFERENCES PRODUCTO(PROD_ID)
37 )'
38 );
39}
40

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/bdCrea.php";
6require_once __DIR__ . "/productoCuenta.php";
7require_once __DIR__ . "/productoAgrega.php";
8require_once __DIR__ . "/ventaCuenta.php";
9require_once __DIR__ . "/ventaAgrega.php";
10
11class Bd
12{
13
14 private static ?PDO $conexion = null;
15
16 public static function getConexion(): PDO
17 {
18 if (self::$conexion === null) {
19 self::$conexion = new PDO(
20 // cadena de conexión
21 "sqlite:srvcompras.db",
22 // usuario
23 null,
24 // contraseña
25 null,
26 // Opciones: conexiones persistentes y lanza excepciones.
27 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
28 );
29
30 bdCrea(self::$conexion);
31 if (productoCuenta() === 0) {
32 productoAgrega(
33 new Producto(nombre: "Sandwich", existencias: 50, precio: 15)
34 );
35 productoAgrega(
36 new Producto(nombre: "Hot dog", existencias: 40, precio: 30)
37 );
38 productoAgrega(
39 new Producto(nombre: "Hamburguesa", existencias: 30, precio: 40)
40 );
41 }
42
43 if (ventaCuenta() === 0) {
44 ventaAgrega(new Venta(enCaptura: true));
45 }
46 }
47
48 return self::$conexion;
49 }
50}
51

3. srv / bd / detalleDeVentaAgrega.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/ventaEnCapturaBusca.php";
7require_once __DIR__ . "/productoBusca.php";
8
9function detalleDeVentaAgrega(DetalleDeVenta $modelo)
10{
11 $con = Bd::getConexion();
12 $producto = productoBusca($modelo->producto->id);
13 if ($producto === false) {
14 $htmlId = htmlentities($modelo->producto->id);
15 throw new ProblemDetails(
16 status: ProblemDetails::BadRequest,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 }
22 $venta = ventaEnCapturaBusca();
23 if ($venta === false)
24 throw new ProblemDetails(
25 status: ProblemDetails::BadRequest,
26 type: "/error/ventaencapturanoencontrada.html",
27 title: "Venta en captura no encontrada.",
28 detail: "No se encontró ninguna venta en captura.",
29 );
30 $modelo->venta = $venta;
31 $modelo->precio = $producto->precio;
32 $modelo->producto = $producto;
33 $modelo->valida();
34 $stmt = $con->prepare(
35 "INSERT INTO DET_VENTA
36 (VENT_ID, PROD_ID, DTV_CANTIDAD, DTV_PRECIO)
37 VALUES
38 (:ventId, :prodId, :cantidad, :precio)"
39 );
40 $stmt->execute(
41 [
42 ":ventId" => $venta->id,
43 ":prodId" => $producto->id,
44 ":cantidad" => $modelo->cantidad,
45 ":precio" => $producto->precio
46 ]
47 );
48}
49

4. srv / bd / detalleDeVentaBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/ventaEnCapturaBusca.php";
6require_once __DIR__ . "/productoBusca.php";
7
8function detalleDeVentaBusca(int $prodId)
9{
10 $venta = ventaEnCapturaBusca();
11 if ($venta === false) {
12 return false;
13 }
14 $producto = productoBusca($prodId);
15 if ($producto === false) {
16 return false;
17 }
18 $con = Bd::getConexion();
19 $stmt = $con->prepare(
20 "SELECT
21 DV.PROD_ID AS prodId,
22 P.PROD_NOMBRE AS prodNombre,
23 DV.DTV_CANTIDAD AS cantidad,
24 DV.DTV_PRECIO AS precio
25 FROM DET_VENTA DV, PRODUCTO P
26 WHERE
27 DV.PROD_ID = P.PROD_ID
28 AND DV.VENT_ID = :ventId
29 AND DV.PROD_ID = :prodId"
30 );
31 $stmt->execute([
32 ":ventId" => $venta->id,
33 ":prodId" => $prodId
34 ]);
35 $stmt->setFetchMode(PDO::FETCH_OBJ);
36 $obj = $stmt->fetch();
37 if ($obj === false) {
38 return false;
39 } else {
40 $dtv = new DetalleDeVenta();
41 $dtv->venta = $venta;
42 $dtv->producto = $producto;
43 $dtv->cantidad = $obj->cantidad;
44 $dtv->precio = $obj->precio;
45 return $dtv;
46 }
47}
48

5. srv / bd / detalleDeVentaConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Venta.php";
5require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
6require_once __DIR__ . "/../modelo/Producto.php";
7require_once __DIR__ . "/Bd.php";
8
9function detalleDeVentaConsulta(Venta $venta)
10{
11 $con = Bd::getConexion();
12 $stmt = $con->query(
13 "SELECT
14 DV.PROD_ID AS prodId,
15 P.PROD_NOMBRE AS prodNombre,
16 P.PROD_EXISTENCIAS AS prodExistencias,
17 P.PROD_PRECIO AS prodPrecio,
18 DV.DTV_CANTIDAD AS cantidad,
19 DV.DTV_PRECIO AS precio
20 FROM DET_VENTA DV, PRODUCTO P
21 WHERE
22 DV.PROD_ID = P.PROD_ID
23 AND DV.VENT_ID = :ventId
24 ORDER BY P.PROD_NOMBRE"
25 );
26 $stmt->execute([":ventId" => $venta->id]);
27 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
28 $objs = recibeFetchAll($resultado);
29 /** @var DetalleDeVenta[] */
30 $detalles = [];
31 foreach ($objs as $obj) {
32 $producto = new Producto(
33 id: $obj->prodId,
34 nombre: $obj->prodNombre,
35 existencias: $obj->prodExistencias,
36 precio: $obj->prodPrecio
37 );
38 $detalle = new DetalleDeVenta(
39 venta: $venta,
40 producto: $producto,
41 cantidad: $obj->cantidad,
42 precio: $obj->precio
43 );
44 $detalles[] = $detalle;
45 }
46 return $detalles;
47}
48

6. srv / bd / detalleDeVentaElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/ventaEnCapturaBusca.php";
5
6function detalleDeVentaElimina(int $prodId)
7{
8 $venta = ventaEnCapturaBusca();
9 if ($venta !== false) {
10 $con = Bd::getConexion();
11 $stmt = $con->prepare(
12 "DELETE FROM DET_VENTA
13 WHERE VENT_ID = :ventId
14 AND PROD_ID = :prodId"
15 );
16 $stmt->execute([
17 ":ventId" => $venta->id,
18 ":prodId" => $prodId,
19 ]);
20 }
21}
22

7. srv / bd / detalleDeVentaModifica.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/../modelo/DetalleDeVenta.php";
5require_once __DIR__ . "/Bd.php";
6require_once __DIR__ . "/ventaEnCapturaBusca.php";
7require_once __DIR__ . "/productoBusca.php";
8
9
10function detalleDeVentaModifica(DetalleDeVenta $modelo)
11{
12 $con = Bd::getConexion();
13 $producto = productoBusca($modelo->producto->id);
14 if ($producto === false) {
15 $htmlId = htmlentities($modelo->producto->id);
16 throw new ProblemDetails(
17 status: ProblemDetails::BadRequest,
18 type: "/error/productonoencontrado.html",
19 title: "Producto no encontrado.",
20 detail: "No se encontró ningún producto con el id $htmlId.",
21 );
22 }
23 $venta = ventaEnCapturaBusca();
24 $venta = ventaEnCapturaBusca();
25 if ($venta === false)
26 throw new ProblemDetails(
27 status: ProblemDetails::BadRequest,
28 type: "/error/ventaencapturanoencontrada.html",
29 title: "Venta en captura no encontrada.",
30 detail: "No se encontró ninguna venta en captura.",
31 );
32 $modelo->venta = $venta;
33 $modelo->producto = $producto;
34 $modelo->precio = $producto->precio;
35 $modelo->valida();
36 $stmt = $con->prepare(
37 "UPDATE DET_VENTA
38 SET
39 DTV_CANTIDAD = :cantidad,
40 DTV_PRECIO = :precio
41 WHERE
42 VENT_ID = :ventId
43 AND PROD_ID = :prodId"
44 );
45 $stmt->execute(
46 [
47 ":ventId" => $venta->id,
48 ":prodId" => $producto->id,
49 ":cantidad" => $modelo->cantidad,
50 ":precio" => $modelo->precio
51 ]
52 );
53}
54

8. srv / bd / productoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5
6function productoAgrega(Producto $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO PRODUCTO
12 (PROD_NOMBRE, PROD_EXISTENCIAS, PROD_PRECIO)
13 VALUES
14 (:nombre, :existencias, :precio)"
15 );
16 $stmt->execute([
17 ":nombre" => $modelo->nombre,
18 ":existencias" => $modelo->existencias,
19 ":precio" => $modelo->precio
20 ]);
21 /* Si usas una secuencia para generar el id,
22 * pasa como parámetro de lastInsertId el
23 * nombre de dicha secuencia, debes
24 * ejecutarlo antes del INSERT y pasarle el
25 * id generado al SQL. */
26 $modelo->id = $con->lastInsertId();
27}
28

9. srv / bd / productoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5
6function productoBusca(int $id): false|Producto
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 PROD_ID AS id,
12 PROD_NOMBRE AS nombre,
13 PROD_PRECIO AS precio,
14 PROD_EXISTENCIAS AS existencias
15 FROM PRODUCTO
16 WHERE PROD_ID = :id"
17 );
18 $stmt->execute([":id" => $id]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Producto::class
22 );
23 return $stmt->fetch();
24}
25

10. srv / bd / productoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Producto[] */
8function productoConsulta(): array
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PROD_ID as id,
14 PROD_NOMBRE as nombre,
15 PROD_PRECIO as precio,
16 PROD_EXISTENCIAS as existencias
17 FROM PRODUCTO
18 ORDER BY PROD_NOMBRE"
19 );
20 $resultado = $stmt->fetchAll(
21 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
22 Producto::class
23 );
24 return recibeFetchAll($resultado);
25}
26

11. srv / bd / productoCuenta.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function productoCuenta(): false|int
6{
7 $con = Bd::getConexion();
8 $stmt = $con->query("SELECT COUNT(*) FROM PRODUCTO");
9 return $stmt->fetchColumn();
10}
11

12. srv / bd / ventaAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5
6function ventaAgrega(Venta $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO VENTA
12 (VENT_EN_CAPTURA)
13 VALUES
14 (:enCaptura)"
15 );
16 $stmt->execute(([":enCaptura" => $modelo->enCaptura]));
17 /* Si usas una secuencia para generar el id,
18 * pasa como parámetro de lastInsertId el
19 * nombre de dicha secuencia, debes
20 * ejecutarlo antes del INSERT y pasarle el
21 * id generado al SQL. */
22 $modelo->id = $con->lastInsertId();
23}
24

13. srv / bd / ventaCuenta.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function ventaCuenta(): false|int
6{
7 $con = Bd::getConexion();
8 $stmt = $con->query("SELECT COUNT(*) FROM VENTA");
9 return $stmt->fetchColumn();
10}
11

14. srv / bd / ventaEnCapturaBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/detalleDeVentaConsulta.php";
6
7function ventaEnCapturaBusca()
8{
9 $con = Bd::getConexion();
10 $stmt = $con->query(
11 "SELECT VENT_ID as id
12 FROM VENTA
13 WHERE VENT_EN_CAPTURA = 1"
14 );
15 $stmt->setFetchMode(PDO::FETCH_OBJ);
16 $obj = $stmt->fetch();
17 if ($obj === false) {
18 return false;
19 } else {
20 $venta = new Venta(id: $obj->id, enCaptura: true);
21 $venta->detalles = detalleDeVentaConsulta($venta);
22 return $venta;
23 }
24}
25

15. srv / bd / ventaEnCapturaProcesa.php

1<?php
2
3require_once __DIR__ . "/../modelo/Venta.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/ventaEnCapturaBusca.php";
6require_once __DIR__ . "/ventaAgrega.php";
7
8function ventaEnCapturaProcesa()
9{
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $modelo = ventaEnCapturaBusca();
13 if ($modelo === false)
14 throw new Exception("Venta no encontrada.");
15 $modelo->valida();
16 $detalles = $modelo->detalles;
17 $stmt = $con->prepare(
18 "UPDATE PRODUCTO
19 SET PROD_EXISTENCIAS = :existencias
20 WHERE PROD_ID = :prodId"
21 );
22 foreach ($detalles as $dtv) {
23 $producto = $dtv->producto;
24 $stmt->execute(([
25 ":prodId" => $producto->id,
26 ":existencias" => $producto->existencias - $dtv->cantidad
27 ]));
28 }
29 $stmt = $con->prepare(
30 "UPDATE VENTA
31 SET VENT_EN_CAPTURA = 0
32 WHERE VENT_ID = :id"
33 );
34 $stmt->execute([":id" => $modelo->id]);
35 ventaAgrega(new Venta(enCaptura: true));
36 $con->commit();
37}
38

M. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

3. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

6. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

7. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeArray.php

1<?php
2
3/**
4 * Devuelve los valores asociados a un
5 * parámetro multivaluado; por ejemplo, un
6 * grupo de checkbox, recibido en el servidor
7 * por medio de GET, POST o cookie. Si no se
8 * recibe el parámetro, devuelve null. Si el
9 * valor recibido no es un arreglo, lo coloca
10 * dentro de uno.
11 */
12function leeArray(string $parametro)
13{
14 if (isset($_REQUEST[$parametro])) {
15 $valor = $_REQUEST[$parametro];
16 return is_array($valor)
17 ? $valor
18 : [$valor];
19 } else {
20 return null;
21 }
22}
23

4. lib / php / leeDecimal.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Recupera el valor decimal de un parámetro
7 * (que puede tener fracciones) enviado al
8 * servidor por medio de GET, POST o cookie.
9 * Si parámetro no se puede convertir a decimal,
10 * se genera un error. Si el parámetro no se
11 * recibe, devuelve null.
12 * Para que funcione parecido a leeEntero,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeDecimal(string $parametro): ?float
17{
18 $valor = leeTexto($parametro);
19 return $valor === null|| $valor === ""
20 ? null
21 : trim($valor);
22}
23

5. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

6. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

7. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

8. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

9. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

10. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function validaNombre(string $nombre)
6{
7 if ($nombre === "")
8 throw new ProblemDetails(
9 status: ProblemDetails::BadRequest,
10 type: "/error/faltanombre.html",
11 title: "Falta el nombre.",
12 );
13}
14

N. Carpeta « error »

Versión para imprimir.

A. error / cantidadincorrecta.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>La cantidad no puede ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>La cantidad no puede ser NAN</h1>
16
17</body>
18
19</html>

B. error / detalledeventaincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Tipo incorrecto para un etalle de venta</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un etalle de venta</h1>
16
17</body>
18
19</html>

C. error / detalledeventanoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Detalle de venta no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Detalle de venta no encontrado</h1>
16
17 <p>No se encontró ningún detalle de venta con el id de producto solicitado.</p>
18
19</body>
20
21</html>

D. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

E. error / existenciasincorrectas.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Las existencias no pueden ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>Las existencias no pueden ser NAN</h1>
16
17</body>
18
19</html>

F. error / faltacantidad.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta la cantidad</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la cantidad</h1>
16
17</body>
18
19</html>

G. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

H. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

I. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

J. error / precioincorrecto.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El precio no puede ser NAN</title>
10
11</head>
12
13<body>
14
15 <h1>El precio no puede ser NAN</h1>
16
17</body>
18
19</html>

K. error / productonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Producto no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Producto no encontrado</h1>
16
17 <p>No se encontró ningún producto con el id solicitado.</p>
18
19</body>
20
21</html>

L. error / ventaencapturanoencontrada.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Venta en captura no encontrada</title>
10
11</head>
12
13<body>
14
15 <h1>Venta en captura no encontrada</h1>
16
17 <p>No se encontró ninguna venta en captura.</p>
18
19</body>
20
21</html>

O. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

P. Resumen