En esta lección se muestra la base de una aplicación que vende o compra.
Para poder manejar compras en línea, es necesario que la venta incluya la referencia al cliente.
Puedes probar la app en https://replit.com/@GilbertoPachec5/srvcompras?v=1. Hazle fork al proyecto y córrelo.
Para este ejemplo se utilizan algunos principios de arquitecturas limpias.
Cada uno de los paquetes apunta con una flecha use
a los que utiliza
para realizar sus funciones.
Cada paquete oculta los detalles de su implementación y tecnología.
Los detalles de la base de datos, así como de su configuración, se mantienen
dentro del paquete bd
y no se exponen fuera de dicho paquete.
Los detalles de la interfaz gráfica, por ejemplo las api del navegador web,
o de las interfaces en Android, se mantienen dentro del paquete
access
y no se exponen fuera de dicho paquete.
El intercambio de datos entre los paquetes access
y service
se realiza de acuerdo al contenido de las lecciones anteriores.
El intercambio de datos entre los paquetes service
y bd
se realiza con el contenido del paquete modelo
.
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.
Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.
Haz clic en los triángulos para expandir las carpetas
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> |
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> |
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> |
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> |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/Venta.php"; |
5 | require_once __DIR__ . "/Producto.php"; |
6 | |
7 | class 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/../../lib/php/validaNombre.php"; |
5 | |
6 | class 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/DetalleDeVenta.php"; |
5 | |
6 | class 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/JsonResponse.php"; |
5 | require_once __DIR__ . "/../lib/php/leeEntero.php"; |
6 | require_once __DIR__ . "/../lib/php/leeDecimal.php"; |
7 | require_once __DIR__ . "/../lib/php/pdFaltaId.php"; |
8 | require_once __DIR__ . "/modelo/Producto.php"; |
9 | require_once __DIR__ . "/modelo/DetalleDeVenta.php"; |
10 | require_once __DIR__ . "/bd/detalleDeVentaAgrega.php"; |
11 | |
12 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
5 | require_once __DIR__ . "/../lib/php/pdFaltaId.php"; |
6 | require_once __DIR__ . "/../lib/php/leeEntero.php"; |
7 | require_once __DIR__ . "/bd/detalleDeVentaBusca.php"; |
8 | |
9 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/JsonResponse.php"; |
5 | require_once __DIR__ . "/../lib/php/pdFaltaId.php"; |
6 | require_once __DIR__ . "/../lib/php/leeEntero.php"; |
7 | require_once __DIR__ . "/bd/detalleDeVentaElimina.php"; |
8 | |
9 | ejecutaServicio(function () { |
10 | $prodId = leeEntero("prodId"); |
11 | if ($prodId === null) throw pdFaltaId(); |
12 | detalleDeVentaElimina($prodId); |
13 | return JsonResponse::noContent(); |
14 | }); |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/pdFaltaId.php"; |
5 | require_once __DIR__ . "/../lib/php/leeEntero.php"; |
6 | require_once __DIR__ . "/../lib/php/leeDecimal.php"; |
7 | require_once __DIR__ . "/modelo/Producto.php"; |
8 | require_once __DIR__ . "/modelo/DetalleDeVenta.php"; |
9 | require_once __DIR__ . "/bd/detalleDeVentaModifica.php"; |
10 | |
11 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
5 | require_once __DIR__ . "/../lib/php/pdFaltaId.php"; |
6 | require_once __DIR__ . "/../lib/php/leeEntero.php"; |
7 | require_once __DIR__ . "/bd/productoBusca.php"; |
8 | |
9 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/bd/productoConsulta.php"; |
5 | |
6 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
5 | require_once __DIR__ . "/modelo/DetalleDeVenta.php"; |
6 | require_once __DIR__ . "/bd/ventaEnCapturaBusca.php"; |
7 | |
8 | ejecutaServicio(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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/bd/ventaEnCapturaProcesa.php"; |
5 | |
6 | ejecutaServicio(function () { |
7 | ventaEnCapturaProcesa(); |
8 | return JsonResponse::created("/srv/srvVentaEnCapturaBusca.php", []); |
9 | }); |
10 |
1 | <?php |
2 | |
3 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Venta.php"; |
4 | require_once __DIR__ . "/../modelo/Producto.php"; |
5 | require_once __DIR__ . "/bdCrea.php"; |
6 | require_once __DIR__ . "/productoCuenta.php"; |
7 | require_once __DIR__ . "/productoAgrega.php"; |
8 | require_once __DIR__ . "/ventaCuenta.php"; |
9 | require_once __DIR__ . "/ventaAgrega.php"; |
10 | |
11 | class 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/../modelo/DetalleDeVenta.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/ventaEnCapturaBusca.php"; |
7 | require_once __DIR__ . "/productoBusca.php"; |
8 | |
9 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/DetalleDeVenta.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/ventaEnCapturaBusca.php"; |
6 | require_once __DIR__ . "/productoBusca.php"; |
7 | |
8 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/recibeFetchAll.php"; |
4 | require_once __DIR__ . "/../modelo/Venta.php"; |
5 | require_once __DIR__ . "/../modelo/DetalleDeVenta.php"; |
6 | require_once __DIR__ . "/../modelo/Producto.php"; |
7 | require_once __DIR__ . "/Bd.php"; |
8 | |
9 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/Bd.php"; |
4 | require_once __DIR__ . "/ventaEnCapturaBusca.php"; |
5 | |
6 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
4 | require_once __DIR__ . "/../modelo/DetalleDeVenta.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/ventaEnCapturaBusca.php"; |
7 | require_once __DIR__ . "/productoBusca.php"; |
8 | |
9 | |
10 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Producto.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | |
6 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Producto.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | |
6 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/recibeFetchAll.php"; |
4 | require_once __DIR__ . "/../modelo/Producto.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | |
7 | /** @return Producto[] */ |
8 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/Bd.php"; |
4 | |
5 | function productoCuenta(): false|int |
6 | { |
7 | $con = Bd::getConexion(); |
8 | $stmt = $con->query("SELECT COUNT(*) FROM PRODUCTO"); |
9 | return $stmt->fetchColumn(); |
10 | } |
11 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Venta.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | |
6 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/Bd.php"; |
4 | |
5 | function ventaCuenta(): false|int |
6 | { |
7 | $con = Bd::getConexion(); |
8 | $stmt = $con->query("SELECT COUNT(*) FROM VENTA"); |
9 | return $stmt->fetchColumn(); |
10 | } |
11 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Venta.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/detalleDeVentaConsulta.php"; |
6 | |
7 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../modelo/Venta.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/ventaEnCapturaBusca.php"; |
6 | require_once __DIR__ . "/ventaAgrega.php"; |
7 | |
8 | function 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 |
1 | export function confirmaEliminar() { |
2 | return confirm("Confirma la eliminación") |
3 | } |
4 | |
5 | // Permite que los eventos de html usen la función. |
6 | window["confirmaEliminar"] = confirmaEliminar |
1 | import { |
2 | JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK |
3 | } from "./JsonResponse.js" |
4 | import { |
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 | */ |
15 | export 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. |
64 | window["invocaServicio"] = invocaServicio |
1 | export const JsonResponse_OK = 200 |
2 | export const JsonResponse_Created = 201 |
3 | export const JsonResponse_NoContent = 204 |
4 | |
5 | export 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 | } |
1 | import { 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 | */ |
8 | export 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. |
31 | window["muestraError"] = muestraError |
1 | /** |
2 | * @param { Document | HTMLElement } raizHtml |
3 | * @param { any } objeto |
4 | */ |
5 | export 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. |
24 | window["muestraObjeto"] = muestraObjeto |
25 | |
26 | /** |
27 | * @param { Document | HTMLElement } raizHtml |
28 | * @param { string } nombre |
29 | */ |
30 | export 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 | */ |
40 | function 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 | */ |
68 | async 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 | */ |
104 | export 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. |
113 | window["getArchivoSeleccionado"] = getArchivoSeleccionado |
114 | |
115 | |
116 | /** |
117 | * @param {HTMLInputElement} input |
118 | * @returns {Promise<string>} |
119 | */ |
120 | export 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. |
145 | window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion |
146 | |
147 | /** |
148 | * @param { Document | HTMLElement } raizHtml |
149 | * @param { HTMLElement } elementoHtml |
150 | */ |
151 | export 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 | } |
1 | export const ProblemDetails_BadRequest = 400 |
2 | export const ProblemDetails_NotFound = 404 |
3 | export const ProblemDetails_InternalServerError = 500 |
4 | |
5 | export 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 | } |
1 | import { 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 | */ |
11 | export 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. |
24 | window["submitForm"] = submitForm |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/JsonResponse.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | /** |
7 | * Ejecuta una funcion que implementa un servicio. |
8 | */ |
9 | function 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 | |
29 | function 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 | |
50 | function 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 | |
69 | function 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 |
1 | <?php |
2 | |
3 | const JsonResponse_OK = 200; |
4 | const JsonResponse_Created = 201; |
5 | const JsonResponse_NoContent = 204; |
6 | |
7 | class 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 |
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 | */ |
12 | function 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 |
1 | <?php |
2 | |
3 | require_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 | */ |
16 | function leeDecimal(string $parametro): ?float |
17 | { |
18 | $valor = leeTexto($parametro); |
19 | return $valor === null|| $valor === "" |
20 | ? null |
21 | : trim($valor); |
22 | } |
23 |
1 | <?php |
2 | |
3 | require_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 | */ |
16 | function leeEntero(string $parametro): ?int |
17 | { |
18 | $valor = leeTexto($parametro); |
19 | return $valor === null || $valor === "" |
20 | ? null |
21 | : trim($valor); |
22 | } |
23 |
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 | */ |
8 | function 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 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | |
5 | function 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 |
1 | <?php |
2 | |
3 | class 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 |
1 | <?php |
2 | |
3 | function recibeFetchAll(false|array $resultado): array |
4 | { |
5 | if ($resultado === false) { |
6 | return []; |
7 | } else { |
8 | return $resultado; |
9 | } |
10 | } |
11 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | |
5 | function 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 |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
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> |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
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 | } |
En esta lección se mostró la base de una aplicación que vende o compra.