From 119fc0dad5d4004322587def9144ab709c64708a Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 7 Nov 2024 00:13:42 -0500 Subject: [PATCH 01/44] WIP admin JS rewrite. --- .gitignore | 1 + registration/frontend/esbuild.mjs | 25 + registration/frontend/package-lock.json | 1718 +++++++++++++++++ registration/frontend/package.json | 9 + registration/frontend/src/admin/cart.tsx | 240 +++ registration/frontend/src/admin/mqtt.ts | 99 + .../frontend/src/admin/scan-actions.tsx | 130 ++ .../frontend/src/entrypoints/admin.ts | 37 + registration/frontend/tsconfig.json | 6 + .../templates/registration/master_admin.html | 1 + .../templates/registration/onsite-admin.html | 52 +- registration/views/onsite_admin.py | 24 +- 12 files changed, 2292 insertions(+), 50 deletions(-) create mode 100644 registration/frontend/esbuild.mjs create mode 100644 registration/frontend/package-lock.json create mode 100644 registration/frontend/package.json create mode 100644 registration/frontend/src/admin/cart.tsx create mode 100644 registration/frontend/src/admin/mqtt.ts create mode 100644 registration/frontend/src/admin/scan-actions.tsx create mode 100644 registration/frontend/src/entrypoints/admin.ts create mode 100644 registration/frontend/tsconfig.json diff --git a/.gitignore b/.gitignore index 39f60a3c..53f8eb53 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ venv3/ .env htmlcov/ .DS_Store +node_modules/ diff --git a/registration/frontend/esbuild.mjs b/registration/frontend/esbuild.mjs new file mode 100644 index 00000000..b3d48b74 --- /dev/null +++ b/registration/frontend/esbuild.mjs @@ -0,0 +1,25 @@ +import * as esbuild from "esbuild"; +import { solidPlugin } from "esbuild-plugin-solid"; + +const IS_PROD = process.env.NODE_ENVIRONMENT === "production"; + +function buildOpts(entryPoints) { + return { + entryPoints, + bundle: true, + outdir: "../static/", + minify: IS_PROD, + sourcemap: true, + drop: IS_PROD ? ["console"] : [], + target: ["es2020"], + loader: { + ".woff": "file", + ".woff2": "file", + }, + plugins: [solidPlugin()], + }; +} + +await Promise.all([ + esbuild.build(buildOpts(["src/entrypoints/admin.ts"])), +]); diff --git a/registration/frontend/package-lock.json b/registration/frontend/package-lock.json new file mode 100644 index 00000000..cbd8a558 --- /dev/null +++ b/registration/frontend/package-lock.json @@ -0,0 +1,1718 @@ +{ + "name": "frontend", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "esbuild": "^0.24.0", + "esbuild-plugin-solid": "^0.6.0", + "mitt": "^3.0.1", + "mqtt": "^5.10.1", + "solid-js": "^1.9.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", + "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", + "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", + "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", + "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", + "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-simple-access": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", + "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz", + "integrity": "sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-typescript": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@types/readable-stream": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", + "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.39.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.39.3.tgz", + "integrity": "sha512-6RzmSu21zYPlV2gNwzjGG9FgODtt9hIWnx7L//OIioIEuRcnpDZoY8Tr+I81Cy1SrH4qoDyKpwHHo6uAMAeyPA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2", + "validate-html-nesting": "^1.2.1" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.3.tgz", + "integrity": "sha512-jvlx5wDp8s+bEF9sGFw/84SInXOA51ttkUEroQziKMbxplXThVKt83qB6bDTa1HuLNatdU9FHpFOiQWs1tLQIg==", + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.39.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.16.tgz", + "integrity": "sha512-V/kz+z2Mx5/6qDfRCilmrukUXcXuCoXKg3/3hDvzKKoSUx8CJKudfIoT29XZc3UE9xBvxs5qictiHdprwtteEg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001677", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", + "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.52", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", + "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==", + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/esbuild-plugin-solid": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/esbuild-plugin-solid/-/esbuild-plugin-solid-0.6.0.tgz", + "integrity": "sha512-V1FvDALwLDX6K0XNYM9CMRAnMzA0+Ecu55qBUT9q/eAJh1KIDsTMFoOzMSgyHqbOfvrVfO3Mws3z7TW2GVnIZA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/preset-typescript": "^7.18.6", + "babel-preset-solid": "^1.6.9" + }, + "peerDependencies": { + "esbuild": ">=0.20", + "solid-js": ">= 1.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mqtt": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.1.tgz", + "integrity": "sha512-hXCOki8sANoQ7w+2OzJzg6qMBxTtrH9RlnVNV8panLZgnl+Gh0J/t4k6r8Az8+C7y3KAcyXtn0mmLixyUom8Sw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.5", + "@types/ws": "^8.5.9", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.3.4", + "help-me": "^5.0.0", + "lru-cache": "^10.0.1", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.0", + "number-allocator": "^1.0.14", + "readable-stream": "^4.4.2", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^4.2.0", + "worker-timers": "^7.1.4", + "ws": "^8.17.1" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.0.tgz", + "integrity": "sha512-8v+HkX+fwbodsWAZIZTI074XIoxVBOmPeggQuDFCGg1SqNcC+uoRMWu7J6QlJPqIUIJXmjNYYHxBBLr1Y/Df4w==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" + }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz", + "integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz", + "integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/solid-js": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.3.tgz", + "integrity": "sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "^1.1.0", + "seroval-plugins": "^1.1.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/validate-html-nesting": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz", + "integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==", + "license": "ISC" + }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/registration/frontend/package.json b/registration/frontend/package.json new file mode 100644 index 00000000..71d07530 --- /dev/null +++ b/registration/frontend/package.json @@ -0,0 +1,9 @@ +{ + "dependencies": { + "esbuild": "^0.24.0", + "esbuild-plugin-solid": "^0.6.0", + "mitt": "^3.0.1", + "mqtt": "^5.10.1", + "solid-js": "^1.9.3" + } +} diff --git a/registration/frontend/src/admin/cart.tsx b/registration/frontend/src/admin/cart.tsx new file mode 100644 index 00000000..fabd42b3 --- /dev/null +++ b/registration/frontend/src/admin/cart.tsx @@ -0,0 +1,240 @@ +import { Accessor, Component, createSignal, For, Setter, Show } from "solid-js"; +import { render } from "solid-js/web"; + +import { ApisConfig, ApisUrls } from "../entrypoints/admin"; + +const CSRF_TOKEN = document.querySelector( + "meta[name='csrf_token']" +).content; + +export let cartManager: CartManager | null = null; +const [cartEntries, setCartEntries] = createSignal(null); + +class CartManager { + urls: ApisUrls; + cartEntries: Accessor; + setCartEntries: Setter; + + constructor( + urls: ApisUrls, + cartEntries: Accessor, + setCartEntries: Setter + ) { + this.urls = urls; + this.cartEntries = cartEntries; + this.setCartEntries = setCartEntries; + } + + updateCart(data: CartResponse) { + if (!data.success) { + alert("Failed to update cart"); + window.location.reload(); + return; + } + + this.setCartEntries(data); + } + + async addCartId(id: string) { + let url = new URL(this.urls.onsite_add_to_cart, window.location.href); + url.search = new URLSearchParams({ id: id.toString() }).toString(); + + const resp = await fetch(url, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + await resp.json(); + + await this.refreshCart(); + } + + async clearCart() { + const resp = await fetch(this.urls.onsite_admin_clear_cart, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + await resp.json(); + + this.setCartEntries(null); + } + + async refreshCart() { + const resp = await fetch(this.urls.onsite_admin_cart, { + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + this.updateCart(data); + } +} + +interface FallibleResponse { + success: boolean; +} + +interface CartResponse extends FallibleResponse { + charityDonation: string; + order_id: number; + orgDonation: string; + reference: string; + subtotal: string; + total: string; + total_discount: string; + result: Badge[]; +} + +interface Badge { + abandoned: string; + age: number; + badgeName: string; + badgeNumber: number | null; + firstName: string; + lastName: string; + holdType: string | null; + printed: boolean; + effectiveLevel: EffectiveLevel; + discount: null; + level_subtotal: string; + level_discount: string; + level_total: string; +} + +interface EffectiveLevel { + name: string; + price: string; +} + +function Cart(cartManager: CartManager) { + return ( +
+ + + + + +
+ ); +} + +const CartEntries: Component<{ cart: CartResponse }> = (props) => { + return ( + <> +
+ + + + + + + + + + + + + + + + + + + + + + + +
Subtotal:{props.cart.subtotal}
Discounts:{props.cart.total_discount}
Donation to Charity:{props.cart.charityDonation}
Donation to Convention:{props.cart.orgDonation}
+ Total: + {`${props.cart.total}`}
+
+ + + {(badge, index) => } + + + ); +}; + +const CartBadge: Component<{ badge: Badge }> = (props) => { + return ( +
+
+ + {props.badge.abandoned} + + + + {props.badge.holdType} + + + + + + + +
+ + + + + + + + + + + + + + + + +
Badge NameLevelPrice
{props.badge.badgeName}{props.badge.effectiveLevel?.name || ""}{props.badge.effectiveLevel?.price || "0.00"}
+
+ ); +}; + +export default function renderCart(config: ApisConfig) { + cartManager = new CartManager(config.urls, cartEntries, setCartEntries); + window["cartManager"] = cartManager; + + render(() => Cart(cartManager), document.getElementById("cart")); +} diff --git a/registration/frontend/src/admin/mqtt.ts b/registration/frontend/src/admin/mqtt.ts new file mode 100644 index 00000000..f848f4ef --- /dev/null +++ b/registration/frontend/src/admin/mqtt.ts @@ -0,0 +1,99 @@ +import mqtt from "mqtt"; +import mitt, { Emitter } from "mitt"; + +import { ApisMqttConfig } from "../entrypoints/admin"; + +type MqttTopic = + | "refresh" + | "open" + | "notification" + | "alert" + | "scan/id" + | "scan/shc"; + +const emitter: Emitter> = mitt(); +window["mqttEmitter"] = emitter; + +const randomClientId = Math.random().toString(16).substr(2, 8); + +function sendNotification(message: string) { + if (Notification.permission === "granted") { + return new Notification(message); + } else { + alert(message); + } +} + +export function connectToMqtt(config: ApisMqttConfig) { + const mqttErrorMessage = document.getElementById("mqtt-client-error"); + + function getTopic(topic: string): string { + return `${config.auth.base_topic}/${topic}`; + } + + const WILDCARD_TOPIC = getTopic("#"); + + const client = mqtt.connect(config.broker, { + username: config.auth.user, + password: config.auth.token, + clientId: `${config.auth.user}-${randomClientId}`, + clean: true, + }); + + client.on("connect", () => { + mqttErrorMessage?.classList?.add("d-none"); + console.debug(`Subscribing to ${WILDCARD_TOPIC}`); + client.subscribe(WILDCARD_TOPIC, (err) => { + if (err) { + console.error(`MQTT subscription failed: ${err}`); + } else { + client.publish(getTopic("admin_presence"), JSON.stringify(":3")); + } + }); + }); + + client.on("error", (err) => { + console.error(`MQTT error: ${err}`); + mqttErrorMessage?.classList?.remove("d-none"); + }); + + client.on("reconnect", () => { + console.debug("Reconnecting to MQTT"); + }); + + client.on("message", (topic, message) => { + console.debug("MQTT message", topic, message); + + let strippedTopic: MqttTopic; + if (topic.startsWith(config.auth.base_topic)) { + strippedTopic = topic.slice(config.auth.base_topic.length + 1) as MqttTopic; + } else { + console.warn(`Got topic with unexpected prefix: ${topic}`); + return; + } + + let data = message.toString(); + let payload = null; + try { + payload = JSON.parse(data); + } catch (err) {} + + switch (strippedTopic) { + case "notification": + if (payload?.["text"]) { + sendNotification(payload?.["text"]); + } + break; + case "alert": + if (payload?.["text"]) { + alert(payload?.["text"]); + } + break; + default: + emitter.emit(strippedTopic, payload); + break; + } + }); +} + +export default emitter; diff --git a/registration/frontend/src/admin/scan-actions.tsx b/registration/frontend/src/admin/scan-actions.tsx new file mode 100644 index 00000000..df249269 --- /dev/null +++ b/registration/frontend/src/admin/scan-actions.tsx @@ -0,0 +1,130 @@ +import { Component, createSignal, Show } from "solid-js"; +import emitter from "./mqtt"; +import { render } from "solid-js/web"; + +interface IdData { + expirationDate: string; + dateOfBirth: string; + age: number; + givenName: string; + familyName: string; +} + +interface ScanLog { + url: string | undefined; + id: IdData | undefined; +} + +const scanPanel = document.getElementById("scan-panel"); +const [scanLog, setScanLog] = createSignal( + { url: undefined, id: undefined }, + { equals: false } +); + +function ScanPanel() { + return ( + <> +
+ Scanner History + +
+ +
+ + + + + + + +
+ + ); +} + +const CloseButton: Component<{ close(): any }> = (props) => { + return ( + + ); +}; + +const UrlEntry: Component<{ url: string }> = (props) => { + return ( +
+
+ {props.url} + setScanLog({ ...scanLog(), url: undefined }) + } /> +
+
+ ); +}; + +const IdEntry: Component<{ data: IdData }> = (props) => { + const expirationDate = new Date(props.data.expirationDate); + const expired = new Date() > expirationDate; + + const panelClasses = { + "panel": true, + "panel-warning": expired, + "panel-primary": !expired + }; + + return ( +
+
+ + License Scanned + + + + {` Expired ${props.data.expirationDate}`} + + + setScanLog({ ...scanLog(), id: undefined }) + } /> +
+
+ {`${props.data.givenName} ${props.data.familyName}`} + + + {` ${props.data.dateOfBirth} (${props.data.age} years)`} + +
+
+ ); +}; + +emitter.on("open", (payload) => { + const url = payload?.["url"]; + if (!url) { + console.error("Open command missing URL"); + return; + } + + window.open(url, "_blank"); + setScanLog({ + ...scanLog(), + url, + }); +}); + +emitter.on("scan/id", (payload: IdData) => { + if (!payload) { + console.error("Missing ID scan payload"); + return; + } + + setScanLog({ + ...scanLog(), + id: payload, + }); +}); + +render(ScanPanel, scanPanel); diff --git a/registration/frontend/src/entrypoints/admin.ts b/registration/frontend/src/entrypoints/admin.ts new file mode 100644 index 00000000..9b2b1ce6 --- /dev/null +++ b/registration/frontend/src/entrypoints/admin.ts @@ -0,0 +1,37 @@ +import { connectToMqtt } from "../admin/mqtt"; +import "../admin/scan-actions"; +import renderCart from "../admin/cart"; + +export interface ApisConfig { + debug: boolean; + printer_uri: string; + mqtt: ApisMqttConfig; + urls: ApisUrls; +} + +export interface ApisMqttConfig { + broker: string; + auth: ApisMqttAuth; +} + +export interface ApisMqttAuth { + user: string; + token: string; + base_topic: string; +} + +export interface ApisUrls { + onsite_admin_clear_cart: string; + onsite_add_to_cart: string; + onsite_admin_cart: string; +} + +declare global { + const APIS_CONFIG: ApisConfig; +} + +if (APIS_CONFIG.mqtt) { + connectToMqtt(APIS_CONFIG.mqtt) +} + +renderCart(APIS_CONFIG); diff --git a/registration/frontend/tsconfig.json b/registration/frontend/tsconfig.json new file mode 100644 index 00000000..c6161116 --- /dev/null +++ b/registration/frontend/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + } +} diff --git a/registration/templates/registration/master_admin.html b/registration/templates/registration/master_admin.html index 0ba88019..141abbb5 100644 --- a/registration/templates/registration/master_admin.html +++ b/registration/templates/registration/master_admin.html @@ -4,6 +4,7 @@ APIS Register + diff --git a/registration/templates/registration/onsite-admin.html b/registration/templates/registration/onsite-admin.html index a2607898..f3b76d2f 100644 --- a/registration/templates/registration/onsite-admin.html +++ b/registration/templates/registration/onsite-admin.html @@ -73,7 +73,7 @@

APIS Register

{% endfor %} -
+
Error: There was a problem while connecting to the server. @@ -141,15 +141,9 @@

APIS Register

-
-
- Scanner history - -
-
-
+
-
+
-
@@ -422,43 +415,8 @@ {% block javascript %} - - - + {% endblock %} diff --git a/registration/views/onsite_admin.py b/registration/views/onsite_admin.py index b3677890..b50db7a3 100644 --- a/registration/views/onsite_admin.py +++ b/registration/views/onsite_admin.py @@ -128,8 +128,26 @@ def onsite_admin(request): "terminals": terminals, "errors": errors, "results": results, - "printer_uri": settings.REGISTER_PRINTER_URI, - "mqtt_auth": mqtt_auth, + "settings": json.dumps({ + "debug": getattr(settings, "DEBUG", False), + "sentry": { + "enabled": getattr(settings, "SENTRY_ENABLED", False), + "user_reports": getattr(settings, "SENTRY_USER_REPORTS", False), + "frontend_dsn": getattr(settings, "SENTRY_FRONTEND_DSN", None), + "environment": getattr(settings, "SENTRY_ENVIRONMENT", None), + "release": getattr(settings, "SENTRY_RELEASE", None), + }, + "printer_uri": settings.REGISTER_PRINTER_URI, + "mqtt": { + "broker": getattr(settings, "MQTT_EXTERNAL_BROKER", None), + "auth": mqtt_auth, + }, + "urls": { + "onsite_admin_clear_cart": reverse("registration:onsite_admin_clear_cart"), + "onsite_add_to_cart": reverse("registration:onsite_add_to_cart"), + "onsite_admin_cart": reverse("registration:onsite_admin_cart"), + } + }), } return render(request, "registration/onsite-admin.html", context) @@ -928,7 +946,7 @@ def onsite_remove_from_cart(request): def onsite_admin_clear_cart(request): request.session["cart"] = [] send_message_to_terminal(request, {"command": "clear"}) - return onsite_admin(request) + return JsonResponse({"success": True, "cart": []}) def get_b32_uuid(): From bc7d431608fa5f9441676f1d02c4ca3a30d41bc2 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 7 Nov 2024 01:22:51 -0500 Subject: [PATCH 02/44] More cart features. --- registration/frontend/src/admin/cart.tsx | 78 +++++++++++++++---- registration/frontend/src/admin/mqtt.ts | 16 +++- .../frontend/src/entrypoints/admin.ts | 4 +- registration/views/onsite_admin.py | 2 + 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/registration/frontend/src/admin/cart.tsx b/registration/frontend/src/admin/cart.tsx index fabd42b3..92a9a05d 100644 --- a/registration/frontend/src/admin/cart.tsx +++ b/registration/frontend/src/admin/cart.tsx @@ -1,4 +1,4 @@ -import { Accessor, Component, createSignal, For, Setter, Show } from "solid-js"; +import { Component, createSignal, For, Setter, Show } from "solid-js"; import { render } from "solid-js/web"; import { ApisConfig, ApisUrls } from "../entrypoints/admin"; @@ -12,16 +12,10 @@ const [cartEntries, setCartEntries] = createSignal(null); class CartManager { urls: ApisUrls; - cartEntries: Accessor; setCartEntries: Setter; - constructor( - urls: ApisUrls, - cartEntries: Accessor, - setCartEntries: Setter - ) { + constructor(urls: ApisUrls, setCartEntries: Setter) { this.urls = urls; - this.cartEntries = cartEntries; this.setCartEntries = setCartEntries; } @@ -72,6 +66,30 @@ class CartManager { this.updateCart(data); } + + async removeBadge(id: number) { + let url = new URL(this.urls.onsite_remove_from_cart, window.location.href); + url.search = new URLSearchParams({ id: id.toString() }).toString(); + + const resp = await fetch(url, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + await resp.json(); + + await this.refreshCart(); + } + + urlForBadge(id: number): string { + let url = new URL( + this.urls.registration_badge_change, + window.location.href + ); + url.pathname = url.pathname.replace("0", id.toString()); + return url.toString(); + } } interface FallibleResponse { @@ -90,6 +108,7 @@ interface CartResponse extends FallibleResponse { } interface Badge { + id: number; abandoned: string; age: number; badgeName: string; @@ -141,13 +160,15 @@ function Cart(cartManager: CartManager) {
- +
); } -const CartEntries: Component<{ cart: CartResponse }> = (props) => { +const CartEntries: Component<{ manager: CartManager; cart: CartResponse }> = ( + props +) => { return ( <>
@@ -180,13 +201,21 @@ const CartEntries: Component<{ cart: CartResponse }> = (props) => {
- {(badge, index) => } + {(badge, index) => ( + + )} ); }; -const CartBadge: Component<{ badge: Badge }> = (props) => { +const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( + props +) => { return (
@@ -210,6 +239,29 @@ const CartBadge: Component<{ badge: Badge }> = (props) => { + + + {props.badge.badgeNumber} + + + + {`${props.badge.firstName} ${props.badge.lastName}`} + + + { + ev.preventDefault(); + props.manager.removeBadge(props.badge.id); + }} + > + + + + + MINOR FORM REQUIRED +
@@ -233,7 +285,7 @@ const CartBadge: Component<{ badge: Badge }> = (props) => { }; export default function renderCart(config: ApisConfig) { - cartManager = new CartManager(config.urls, cartEntries, setCartEntries); + cartManager = new CartManager(config.urls, setCartEntries); window["cartManager"] = cartManager; render(() => Cart(cartManager), document.getElementById("cart")); diff --git a/registration/frontend/src/admin/mqtt.ts b/registration/frontend/src/admin/mqtt.ts index f848f4ef..163202db 100644 --- a/registration/frontend/src/admin/mqtt.ts +++ b/registration/frontend/src/admin/mqtt.ts @@ -24,6 +24,9 @@ function sendNotification(message: string) { } } +export let publishMessage: (topic: string, message: string) => void | null = + null; + export function connectToMqtt(config: ApisMqttConfig) { const mqttErrorMessage = document.getElementById("mqtt-client-error"); @@ -40,6 +43,11 @@ export function connectToMqtt(config: ApisMqttConfig) { clean: true, }); + publishMessage = (topic, message) => { + console.debug(`Publishing to topic ${topic} with message: ${message}`); + client.publish(getTopic(topic), message); + }; + client.on("connect", () => { mqttErrorMessage?.classList?.add("d-none"); console.debug(`Subscribing to ${WILDCARD_TOPIC}`); @@ -62,17 +70,19 @@ export function connectToMqtt(config: ApisMqttConfig) { }); client.on("message", (topic, message) => { - console.debug("MQTT message", topic, message); + let data = message.toString(); + console.debug("MQTT message", topic, data); let strippedTopic: MqttTopic; if (topic.startsWith(config.auth.base_topic)) { - strippedTopic = topic.slice(config.auth.base_topic.length + 1) as MqttTopic; + strippedTopic = topic.slice( + config.auth.base_topic.length + 1 + ) as MqttTopic; } else { console.warn(`Got topic with unexpected prefix: ${topic}`); return; } - let data = message.toString(); let payload = null; try { payload = JSON.parse(data); diff --git a/registration/frontend/src/entrypoints/admin.ts b/registration/frontend/src/entrypoints/admin.ts index 9b2b1ce6..a8c771f2 100644 --- a/registration/frontend/src/entrypoints/admin.ts +++ b/registration/frontend/src/entrypoints/admin.ts @@ -21,9 +21,11 @@ export interface ApisMqttAuth { } export interface ApisUrls { - onsite_admin_clear_cart: string; onsite_add_to_cart: string; onsite_admin_cart: string; + onsite_admin_clear_cart: string; + onsite_remove_from_cart: string; + registration_badge_change: string; } declare global { diff --git a/registration/views/onsite_admin.py b/registration/views/onsite_admin.py index b50db7a3..45b6a1f2 100644 --- a/registration/views/onsite_admin.py +++ b/registration/views/onsite_admin.py @@ -146,6 +146,8 @@ def onsite_admin(request): "onsite_admin_clear_cart": reverse("registration:onsite_admin_clear_cart"), "onsite_add_to_cart": reverse("registration:onsite_add_to_cart"), "onsite_admin_cart": reverse("registration:onsite_admin_cart"), + "onsite_remove_from_cart": reverse("registration:onsite_remove_from_cart"), + "registration_badge_change": reverse("admin:registration_badge_change", args=(0,)), } }), } From 83ce80661255d2586d88ef1c92eadf034c242cb9 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 7 Nov 2024 13:14:03 -0500 Subject: [PATCH 03/44] More features. --- registration/frontend/package-lock.json | 24 ++ registration/frontend/package.json | 4 + registration/frontend/src/admin/cart.tsx | 292 ------------------ .../frontend/src/admin/cart/cart-manager.ts | 230 ++++++++++++++ .../src/admin/cart/components/Cart.tsx | 55 ++++ .../src/admin/cart/components/CartActions.tsx | 167 ++++++++++ .../src/admin/cart/components/CartBadge.tsx | 74 +++++ .../src/admin/cart/components/CartEntries.tsx | 94 ++++++ .../frontend/src/admin/cart/index.tsx | 24 ++ .../src/admin/providers/config-provider.tsx | 5 + .../frontend/src/admin/scan-actions.tsx | 161 ++++++++-- .../frontend/src/entrypoints/admin.ts | 16 +- registration/views/onsite_admin.py | 17 + 13 files changed, 851 insertions(+), 312 deletions(-) delete mode 100644 registration/frontend/src/admin/cart.tsx create mode 100644 registration/frontend/src/admin/cart/cart-manager.ts create mode 100644 registration/frontend/src/admin/cart/components/Cart.tsx create mode 100644 registration/frontend/src/admin/cart/components/CartActions.tsx create mode 100644 registration/frontend/src/admin/cart/components/CartBadge.tsx create mode 100644 registration/frontend/src/admin/cart/components/CartEntries.tsx create mode 100644 registration/frontend/src/admin/cart/index.tsx create mode 100644 registration/frontend/src/admin/providers/config-provider.tsx diff --git a/registration/frontend/package-lock.json b/registration/frontend/package-lock.json index cbd8a558..205d612f 100644 --- a/registration/frontend/package-lock.json +++ b/registration/frontend/package-lock.json @@ -5,11 +5,15 @@ "packages": { "": { "dependencies": { + "big.js": "^6.2.2", "esbuild": "^0.24.0", "esbuild-plugin-solid": "^0.6.0", "mitt": "^3.0.1", "mqtt": "^5.10.1", "solid-js": "^1.9.3" + }, + "devDependencies": { + "@types/big.js": "^6.2.2" } }, "node_modules/@ampproject/remapping": { @@ -888,6 +892,13 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@types/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", @@ -989,6 +1000,19 @@ ], "license": "MIT" }, + "node_modules/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, "node_modules/bl": { "version": "6.0.16", "resolved": "https://registry.npmjs.org/bl/-/bl-6.0.16.tgz", diff --git a/registration/frontend/package.json b/registration/frontend/package.json index 71d07530..4a1745ac 100644 --- a/registration/frontend/package.json +++ b/registration/frontend/package.json @@ -1,9 +1,13 @@ { "dependencies": { + "big.js": "^6.2.2", "esbuild": "^0.24.0", "esbuild-plugin-solid": "^0.6.0", "mitt": "^3.0.1", "mqtt": "^5.10.1", "solid-js": "^1.9.3" + }, + "devDependencies": { + "@types/big.js": "^6.2.2" } } diff --git a/registration/frontend/src/admin/cart.tsx b/registration/frontend/src/admin/cart.tsx deleted file mode 100644 index 92a9a05d..00000000 --- a/registration/frontend/src/admin/cart.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { Component, createSignal, For, Setter, Show } from "solid-js"; -import { render } from "solid-js/web"; - -import { ApisConfig, ApisUrls } from "../entrypoints/admin"; - -const CSRF_TOKEN = document.querySelector( - "meta[name='csrf_token']" -).content; - -export let cartManager: CartManager | null = null; -const [cartEntries, setCartEntries] = createSignal(null); - -class CartManager { - urls: ApisUrls; - setCartEntries: Setter; - - constructor(urls: ApisUrls, setCartEntries: Setter) { - this.urls = urls; - this.setCartEntries = setCartEntries; - } - - updateCart(data: CartResponse) { - if (!data.success) { - alert("Failed to update cart"); - window.location.reload(); - return; - } - - this.setCartEntries(data); - } - - async addCartId(id: string) { - let url = new URL(this.urls.onsite_add_to_cart, window.location.href); - url.search = new URLSearchParams({ id: id.toString() }).toString(); - - const resp = await fetch(url, { - method: "POST", - headers: { - "x-csrftoken": CSRF_TOKEN, - }, - }); - await resp.json(); - - await this.refreshCart(); - } - - async clearCart() { - const resp = await fetch(this.urls.onsite_admin_clear_cart, { - method: "POST", - headers: { - "x-csrftoken": CSRF_TOKEN, - }, - }); - await resp.json(); - - this.setCartEntries(null); - } - - async refreshCart() { - const resp = await fetch(this.urls.onsite_admin_cart, { - headers: { - "x-csrftoken": CSRF_TOKEN, - }, - }); - const data = await resp.json(); - - this.updateCart(data); - } - - async removeBadge(id: number) { - let url = new URL(this.urls.onsite_remove_from_cart, window.location.href); - url.search = new URLSearchParams({ id: id.toString() }).toString(); - - const resp = await fetch(url, { - method: "POST", - headers: { - "x-csrftoken": CSRF_TOKEN, - }, - }); - await resp.json(); - - await this.refreshCart(); - } - - urlForBadge(id: number): string { - let url = new URL( - this.urls.registration_badge_change, - window.location.href - ); - url.pathname = url.pathname.replace("0", id.toString()); - return url.toString(); - } -} - -interface FallibleResponse { - success: boolean; -} - -interface CartResponse extends FallibleResponse { - charityDonation: string; - order_id: number; - orgDonation: string; - reference: string; - subtotal: string; - total: string; - total_discount: string; - result: Badge[]; -} - -interface Badge { - id: number; - abandoned: string; - age: number; - badgeName: string; - badgeNumber: number | null; - firstName: string; - lastName: string; - holdType: string | null; - printed: boolean; - effectiveLevel: EffectiveLevel; - discount: null; - level_subtotal: string; - level_discount: string; - level_total: string; -} - -interface EffectiveLevel { - name: string; - price: string; -} - -function Cart(cartManager: CartManager) { - return ( -
- - - - - -
- ); -} - -const CartEntries: Component<{ manager: CartManager; cart: CartResponse }> = ( - props -) => { - return ( - <> -
-
- - - - - - - - - - - - - - - - - - - - - - -
Subtotal:{props.cart.subtotal}
Discounts:{props.cart.total_discount}
Donation to Charity:{props.cart.charityDonation}
Donation to Convention:{props.cart.orgDonation}
- Total: - {`${props.cart.total}`}
-
- - - {(badge, index) => ( - - )} - - - ); -}; - -const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( - props -) => { - return ( -
-
- - {props.badge.abandoned} - - - - {props.badge.holdType} - - - - - - - - - - {props.badge.badgeNumber} - - - - {`${props.badge.firstName} ${props.badge.lastName}`} - - - { - ev.preventDefault(); - props.manager.removeBadge(props.badge.id); - }} - > - - - - - MINOR FORM REQUIRED - -
- - - - - - - - - - - - - - - - -
Badge NameLevelPrice
{props.badge.badgeName}{props.badge.effectiveLevel?.name || ""}{props.badge.effectiveLevel?.price || "0.00"}
-
- ); -}; - -export default function renderCart(config: ApisConfig) { - cartManager = new CartManager(config.urls, setCartEntries); - window["cartManager"] = cartManager; - - render(() => Cart(cartManager), document.getElementById("cart")); -} diff --git a/registration/frontend/src/admin/cart/cart-manager.ts b/registration/frontend/src/admin/cart/cart-manager.ts new file mode 100644 index 00000000..f688a7a2 --- /dev/null +++ b/registration/frontend/src/admin/cart/cart-manager.ts @@ -0,0 +1,230 @@ +import { Setter } from "solid-js"; + +import { ApisUrls } from "../../entrypoints/admin"; +import emitter, { publishMessage } from "../mqtt"; + +const CSRF_TOKEN = document.querySelector( + "meta[name='csrf_token']" +).content; + +export class CartManager { + urls: ApisUrls; + setCartEntries: Setter; + + constructor(urls: ApisUrls, setCartEntries: Setter) { + this.urls = urls; + this.setCartEntries = setCartEntries; + + // Only one CartManager should be active at a time. + emitter.off("refresh"); + emitter.on("refresh", this.refreshCart.bind(this)); + } + + private updateCart(data: CartResponse) { + if (!data.success) { + alert("Failed to update cart"); + window.location.reload(); + return; + } + + this.setCartEntries(data); + } + + public async addCartId(id: string) { + let url = new URL(this.urls.onsite_add_to_cart, window.location.href); + url.search = new URLSearchParams({ id: id.toString() }).toString(); + + const resp = await fetch(url, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + await resp.json(); + + await this.refreshCart(); + } + + public async clearCart() { + const resp = await fetch(this.urls.onsite_admin_clear_cart, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + await resp.json(); + + this.setCartEntries(null); + } + + public async refreshCart() { + const resp = await fetch(this.urls.onsite_admin_cart, { + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + this.updateCart(data); + } + + public async removeBadge(id: number) { + let url = new URL(this.urls.onsite_remove_from_cart, window.location.href); + url.search = new URLSearchParams({ id: id.toString() }).toString(); + + const resp = await fetch(url, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + if (!data["success"]) { + alert(`Error removing from cart: ${data["reason"]}`); + return; + } + + await this.refreshCart(); + } + + public async applyCashPayment( + reference: string, + total: string, + tendered: string + ): Promise { + let url = new URL( + this.urls.complete_cash_transaction, + window.location.href + ); + url.search = new URLSearchParams({ reference, total, tendered }).toString(); + + const resp = await fetch(url, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + return data; + } + + public async enableCardPayment(): Promise { + const resp = await fetch(this.urls.enable_payment, { + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + return data; + } + + public async printBadges(ids: number[]): Promise { + const assignResp = await fetch(this.urls.assign_badge_number, { + method: "POST", + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + body: JSON.stringify( + ids.map((id) => { + return { + id, + }; + }) + ), + }); + const assignData: FallibleRequest = await assignResp.json(); + + if (!assignData.success) { + return assignData; + } + + let url = new URL( + this.urls.onsite_print_badges, + window.location.href + ); + let params = new URLSearchParams(); + ids.forEach((id) => params.append("id", id.toString())); + url.search = params.toString(); + + const printResp = await fetch(url, { + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const printData: BadgePrintResponse = await printResp.json(); + + await this.clearCart(); + await this.refreshCart(); + + return printData; + } + + public urlForBadge(id: number): string { + let url = new URL( + this.urls.registration_badge_change, + window.location.href + ); + url.pathname = url.pathname.replace("0", id.toString()); + return url.toString(); + } +} + +export interface FallibleRequest { + success: boolean; +} + +export interface CartResponse extends FallibleRequest { + charityDonation: string; + order_id: number; + orgDonation: string; + reference: string; + subtotal: string; + total: string; + total_discount: string; + result: Badge[]; +} + +export interface Badge { + id: number; + abandoned: string; + age: number; + badgeName: string; + badgeNumber: number | null; + firstName: string; + lastName: string; + holdType: string | null; + printed: boolean; + effectiveLevel: EffectiveLevel; + discount: Discount | null; + level_subtotal: string; + level_discount: string; + level_total: string; + attendee_options: AttendeeOption[]; +} + +export interface EffectiveLevel { + name: string; + price: string; +} + +export interface Discount { + name: string; + amount_off: string; + percent_off: string; +} + +export interface AttendeeOption { + quantity: number; + item: string; + price: string; + total: string; +} + +export interface BadgePrintResponse extends FallibleRequest { + file: string; + next: string; + url: string; +} diff --git a/registration/frontend/src/admin/cart/components/Cart.tsx b/registration/frontend/src/admin/cart/components/Cart.tsx new file mode 100644 index 00000000..b8b153af --- /dev/null +++ b/registration/frontend/src/admin/cart/components/Cart.tsx @@ -0,0 +1,55 @@ +import { Show } from "solid-js/web"; +import { Component, createEffect } from "solid-js"; + +import { CartManager, CartResponse } from "../cart-manager"; +import { CartEntries } from "./CartEntries"; +import { CartActions } from "./CartActions"; + +export const Cart: Component<{ + cartManager: CartManager; + cartEntries: CartResponse; +}> = (props) => { + createEffect(() => { + props.cartManager.refreshCart(); + }); + + return ( +
+ + + + + + + +
+ ); +}; diff --git a/registration/frontend/src/admin/cart/components/CartActions.tsx b/registration/frontend/src/admin/cart/components/CartActions.tsx new file mode 100644 index 00000000..812c4cdb --- /dev/null +++ b/registration/frontend/src/admin/cart/components/CartActions.tsx @@ -0,0 +1,167 @@ +import { Component, createEffect, createMemo, useContext } from "solid-js"; +import { Big } from "big.js"; + +import { BadgePrintResponse, CartManager, CartResponse } from "../cart-manager"; +import { ConfigContext } from "../../providers/config-provider"; +import { publishMessage } from "../../mqtt"; + +const PRINTABLE_STATUS = new Set(["Paid", "Comp", "Staff", "Dealer"]); + +async function attemptCashPayment( + manager: CartManager, + reference: string, + total: string +) { + const totalAmount = new Big(total); + + const tendered = prompt("Enter tendered amount"); + let tenderedAmount: Big; + try { + tenderedAmount = new Big(tendered); + } catch (err) { + alert("Invalid amount."); + return; + } + + if (tenderedAmount.lt(totalAmount)) { + alert("Insufficient payment, split tender unsupported."); + return; + } + + let change = tenderedAmount.sub(totalAmount); + + const resp = await manager.applyCashPayment(reference, total, tendered); + if (resp.success) { + manager.refreshCart(); + } else { + alert("Error posting cash transaction."); + return; + } + + alert(`Change: ${change}`); +} + +async function enableCardPayment(manager: CartManager) { + const resp = await manager.enableCardPayment(); + if (!resp.success) { + alert("Error enabling card payment."); + } +} + +async function printBadges( + manager: CartManager, + ids: number[], + mqttPrint: boolean = false, + pdfUrl: string | null = null +) { + const resp = await manager.printBadges(ids); + if (!resp.success) { + alert("Error printing badges."); + return; + } + + // If it was successful, it should always have the correct data. + const data = resp as BadgePrintResponse; + + if (mqttPrint && pdfUrl && publishMessage) { + let url = new URL(pdfUrl, window.location.href); + url.searchParams.append("file", data.file); + + publishMessage("action", JSON.stringify({ + action: "print", + url, + })); + } else { + window.open(data.url, "badge"); + } +} + +export const CartActions: Component<{ + manager: CartManager; + cartEntries: CartResponse; +}> = (props) => { + const config = useContext(ConfigContext); + + const hasHold = createMemo( + () => + props.cartEntries?.result?.some((entry) => !!entry.holdType) || + isNaN(parseFloat(props?.cartEntries?.total)) + ); + + const needsPayment = () => parseFloat(props.cartEntries?.total) > 0; + + const printableBadgeIds = createMemo( + () => + props.cartEntries?.result + ?.filter((badge) => { + const isPrintable = + PRINTABLE_STATUS.has(badge.abandoned) && + !badge.holdType && + !badge.printed; + + return isPrintable; + }) + ?.map((badge) => badge.id) || [] + ); + + createEffect(() => + console.log( + `hold: ${hasHold()}, needsPayment: ${needsPayment()}, printable: ${printableBadgeIds()}` + ) + ); + + const canTenderCash = () => + config.permissions.cash && !hasHold() && needsPayment(); + const canUseCard = () => !hasHold() && needsPayment(); + const hasPrintableBadges = () => printableBadgeIds()?.length > 0 || false; + const canApplyDiscount = () => config.permissions.discount && false; + + return ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/cart/components/CartBadge.tsx b/registration/frontend/src/admin/cart/components/CartBadge.tsx new file mode 100644 index 00000000..45947b85 --- /dev/null +++ b/registration/frontend/src/admin/cart/components/CartBadge.tsx @@ -0,0 +1,74 @@ +import { Component, Show } from "solid-js"; + +import { Badge, CartManager } from "../cart-manager"; + +export const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( + props +) => { + return ( +
+
+ + {props.badge.abandoned} + + + + {props.badge.holdType} + + + + + + + + + + {props.badge.badgeNumber} + + + + {`${props.badge.firstName} ${props.badge.lastName}`} + + + { + ev.preventDefault(); + props.manager.removeBadge(props.badge.id); + }} + > + + + + + MINOR FORM REQUIRED + +
+ + + + + + + + + + + + + + + + +
Badge NameLevelPrice
{props.badge.badgeName}{props.badge.effectiveLevel?.name || ""}{props.badge.effectiveLevel?.price || "0.00"}
+
+ ); +}; diff --git a/registration/frontend/src/admin/cart/components/CartEntries.tsx b/registration/frontend/src/admin/cart/components/CartEntries.tsx new file mode 100644 index 00000000..7bfa3fe0 --- /dev/null +++ b/registration/frontend/src/admin/cart/components/CartEntries.tsx @@ -0,0 +1,94 @@ +import { Component, createMemo, For, Show } from "solid-js"; + +import { CartManager, CartResponse } from "../cart-manager"; +import { CartBadge } from "./CartBadge"; + +export const CartEntries: Component<{ + manager: CartManager; + cart: CartResponse; +}> = (props) => { + const orderItems = createMemo(() => + props.cart.result + .flatMap((result) => { + let options = result.attendee_options; + if (result.discount) { + options.push({ + quantity: 1, + item: `Discount ${result.discount.name}`, + price: `-${result.discount.amount_off} / ${result.discount.percent_off}%`, + total: `-$${result.level_discount}`, + }); + } + return options; + }) + .flat() + ); + + return ( + <> +
+ + + + + + + + + + + + + + + + + + + + + + + +
Subtotal:{props.cart.subtotal}
Discounts:{props.cart.total_discount}
Donation to Charity:{props.cart.charityDonation}
Donation to Convention:{props.cart.orgDonation}
+ Total: + {`${props.cart.total}`}
+
+ + + {(badge, index) => ( + + )} + + + 0}> +
+ + + + + + + + + + {(item, index) => ( + + + + + )} + + +
Order ItemPrice
{`${item.quantity} × ${item.item} (@ ${item.price})`} + {item.total} +
+
+
+ + ); +}; diff --git a/registration/frontend/src/admin/cart/index.tsx b/registration/frontend/src/admin/cart/index.tsx new file mode 100644 index 00000000..3d4ba72d --- /dev/null +++ b/registration/frontend/src/admin/cart/index.tsx @@ -0,0 +1,24 @@ +import { createSignal } from "solid-js"; +import { render } from "solid-js/web"; + +import { CartManager, CartResponse } from "./cart-manager"; +import { ApisConfig } from "../../entrypoints/admin"; +import { Cart } from "./components/Cart"; +import { ConfigContext } from "../providers/config-provider"; + +const [cartEntries, setCartEntries] = createSignal(null); + +export let cartManager: CartManager | null = null; + +export default function createCart(config: ApisConfig) { + cartManager = new CartManager(config.urls, setCartEntries); + window["cartManager"] = cartManager; + + render(() => { + return ( + + + + ); + }, document.getElementById("cart")); +} diff --git a/registration/frontend/src/admin/providers/config-provider.tsx b/registration/frontend/src/admin/providers/config-provider.tsx new file mode 100644 index 00000000..0e4c50c2 --- /dev/null +++ b/registration/frontend/src/admin/providers/config-provider.tsx @@ -0,0 +1,5 @@ +import { createContext } from "solid-js"; + +import { ApisConfig } from "../../entrypoints/admin"; + +export const ConfigContext = createContext(); diff --git a/registration/frontend/src/admin/scan-actions.tsx b/registration/frontend/src/admin/scan-actions.tsx index df249269..a39a3d41 100644 --- a/registration/frontend/src/admin/scan-actions.tsx +++ b/registration/frontend/src/admin/scan-actions.tsx @@ -1,4 +1,4 @@ -import { Component, createSignal, Show } from "solid-js"; +import { Component, createSignal, For, Show } from "solid-js"; import emitter from "./mqtt"; import { render } from "solid-js/web"; @@ -10,25 +10,70 @@ interface IdData { familyName: string; } +interface ShcData { + name: string; + birthday: string; + verification: ShcIssuer; + vaccines: ShcVaccine[]; +} + +interface ShcIssuer { + issuer: string; + verified: boolean; + trusted: boolean; +} + +interface ShcVaccine { + name: string; + lotNumber: string; + status: string; + date: string; + performer: string; +} + interface ScanLog { url: string | undefined; id: IdData | undefined; + shc: ShcData | undefined; +} + +interface ShcMatch { + name: boolean; + dob: boolean; } const scanPanel = document.getElementById("scan-panel"); -const [scanLog, setScanLog] = createSignal( - { url: undefined, id: undefined }, - { equals: false } -); +const [scanLog, setScanLog] = createSignal({ + url: undefined, + id: undefined, + shc: undefined, +}); function ScanPanel() { + const shcMatch = () => { + const data = scanLog(); + if (!data.id || !data.shc) return { name: false, dob: false }; + + const idName = `${data.id.givenName} ${data.id.familyName}`; + const name = idName.localeCompare(data.shc.name) === 0; + + const dob = data.id.dateOfBirth === data.shc.birthday; + + return { name, dob }; + }; + return ( <>
Scanner History - +
@@ -36,6 +81,10 @@ function ScanPanel() { + + + + @@ -56,10 +105,12 @@ const UrlEntry: Component<{ url: string }> = (props) => { return (
- {props.url} - setScanLog({ ...scanLog(), url: undefined }) - } /> + + {props.url} + + setScanLog({ ...scanLog(), url: undefined })} + />
); @@ -70,13 +121,12 @@ const IdEntry: Component<{ data: IdData }> = (props) => { const expired = new Date() > expirationDate; const panelClasses = { - "panel": true, "panel-warning": expired, - "panel-primary": !expired + "panel-primary": !expired, }; return ( -
+
License Scanned @@ -86,9 +136,9 @@ const IdEntry: Component<{ data: IdData }> = (props) => { {` Expired ${props.data.expirationDate}`} - setScanLog({ ...scanLog(), id: undefined }) - } /> + setScanLog({ ...scanLog(), id: undefined })} + />
{`${props.data.givenName} ${props.data.familyName}`} @@ -101,6 +151,69 @@ const IdEntry: Component<{ data: IdData }> = (props) => { ); }; +const ShcEntry: Component<{ data: ShcData; shcMatch: ShcMatch }> = (props) => { + const status = () => { + let status = "Partially Verified"; + if (props.data.verification.trusted) { + status = "Verified"; + } else if (!props.data.verification.verified) { + status = "Not Verified"; + } + return status; + }; + + return ( +
+
+ Vaccination Record - {status()} + setScanLog({ ...scanLog(), shc: undefined })} + /> +
+
+ {props.data.name}} + > + + + {props.data.name} + + + + + + + + + + + + + + {(vaccine, index) => { + return ( + + + + + + ); + }} + + +
DateVaccineLot
{vaccine.date}{vaccine.name}{vaccine.lotNumber}
+
+
+ ); +}; + emitter.on("open", (payload) => { const url = payload?.["url"]; if (!url) { @@ -127,4 +240,16 @@ emitter.on("scan/id", (payload: IdData) => { }); }); +emitter.on("scan/shc", (payload: ShcData) => { + if (!payload) { + console.error("Missing SHC scan payload"); + return; + } + + setScanLog({ + ...scanLog(), + shc: payload, + }); +}); + render(ScanPanel, scanPanel); diff --git a/registration/frontend/src/entrypoints/admin.ts b/registration/frontend/src/entrypoints/admin.ts index a8c771f2..818962d0 100644 --- a/registration/frontend/src/entrypoints/admin.ts +++ b/registration/frontend/src/entrypoints/admin.ts @@ -1,17 +1,19 @@ import { connectToMqtt } from "../admin/mqtt"; import "../admin/scan-actions"; -import renderCart from "../admin/cart"; +import createCart from "../admin/cart"; export interface ApisConfig { debug: boolean; printer_uri: string; mqtt: ApisMqttConfig; urls: ApisUrls; + permissions: ApisPermissions; } export interface ApisMqttConfig { broker: string; auth: ApisMqttAuth; + supports_printing: boolean; } export interface ApisMqttAuth { @@ -21,11 +23,21 @@ export interface ApisMqttAuth { } export interface ApisUrls { + assign_badge_number: string; + complete_cash_transaction: string; + enable_payment: string; onsite_add_to_cart: string; onsite_admin_cart: string; onsite_admin_clear_cart: string; + onsite_print_badges: string; onsite_remove_from_cart: string; registration_badge_change: string; + pdf: string; +} + +export interface ApisPermissions { + cash: boolean; + discount: boolean; } declare global { @@ -36,4 +48,4 @@ if (APIS_CONFIG.mqtt) { connectToMqtt(APIS_CONFIG.mqtt) } -renderCart(APIS_CONFIG); +createCart(APIS_CONFIG); diff --git a/registration/views/onsite_admin.py b/registration/views/onsite_admin.py index 45b6a1f2..a58cbb41 100644 --- a/registration/views/onsite_admin.py +++ b/registration/views/onsite_admin.py @@ -141,13 +141,23 @@ def onsite_admin(request): "mqtt": { "broker": getattr(settings, "MQTT_EXTERNAL_BROKER", None), "auth": mqtt_auth, + "supports_printing": getattr(settings, "PRINT_VIA_MQTT", False), }, "urls": { + "assign_badge_number": reverse("registration:assign_badge_number"), + "onsite_print_badges": reverse("registration:onsite_print_badges"), + "complete_cash_transaction": reverse("registration:complete_cash_transaction"), + "enable_payment": reverse("registration:enable_payment"), "onsite_admin_clear_cart": reverse("registration:onsite_admin_clear_cart"), "onsite_add_to_cart": reverse("registration:onsite_add_to_cart"), "onsite_admin_cart": reverse("registration:onsite_admin_cart"), "onsite_remove_from_cart": reverse("registration:onsite_remove_from_cart"), "registration_badge_change": reverse("admin:registration_badge_change", args=(0,)), + "pdf": reverse("registration:pdf"), + }, + "permissions": { + "cash": request.user.has_perm("registration.cash"), + "discount": request.user.has_perm("registration.discount"), } }), } @@ -907,6 +917,13 @@ def onsite_add_id_to_cart(request, id): {"success": False, "reason": "Need ID parameter"}, status=400 ) + try: + id = int(id) + except ValueError: + return JsonResponse( + {"success": False, "reason": "ID parameter must be integer"}, status=400 + ) + cart = request.session.get("cart", None) if cart is None: request.session["cart"] = [ From 38900eec5b4375837669e6335dd1043fe939781e Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 7 Nov 2024 23:25:35 -0500 Subject: [PATCH 04/44] Almost everything working now. --- registration/frontend/esbuild.mjs | 13 +- registration/frontend/package-lock.json | 1025 ++++++++++++++++- registration/frontend/package.json | 5 + .../frontend/src/admin/attendee-search.tsx | 265 +++++ .../frontend/src/admin/cart/cart-manager.ts | 25 +- .../src/admin/cart/components/Cart.tsx | 82 +- .../src/admin/cart/components/CartActions.tsx | 186 ++- .../src/admin/cart/components/CartBadge.tsx | 124 +- .../src/admin/cart/components/CartEntries.tsx | 67 +- .../frontend/src/admin/cart/index.tsx | 24 - registration/frontend/src/admin/index.scss | 14 + registration/frontend/src/admin/navbar.tsx | 246 ++++ registration/frontend/src/admin/onsite.tsx | 51 + .../frontend/src/admin/scan-actions.tsx | 301 +++-- .../frontend/src/entrypoints/admin.ts | 39 +- .../templates/registration/master_admin.html | 41 +- .../templates/registration/onsite-admin.html | 425 +------ registration/views/onsite_admin.py | 72 +- 18 files changed, 2232 insertions(+), 773 deletions(-) create mode 100644 registration/frontend/src/admin/attendee-search.tsx delete mode 100644 registration/frontend/src/admin/cart/index.tsx create mode 100644 registration/frontend/src/admin/index.scss create mode 100644 registration/frontend/src/admin/navbar.tsx create mode 100644 registration/frontend/src/admin/onsite.tsx diff --git a/registration/frontend/esbuild.mjs b/registration/frontend/esbuild.mjs index b3d48b74..4f05ba57 100644 --- a/registration/frontend/esbuild.mjs +++ b/registration/frontend/esbuild.mjs @@ -1,4 +1,5 @@ import * as esbuild from "esbuild"; +import { sassPlugin } from "esbuild-sass-plugin"; import { solidPlugin } from "esbuild-plugin-solid"; const IS_PROD = process.env.NODE_ENVIRONMENT === "production"; @@ -15,11 +16,15 @@ function buildOpts(entryPoints) { loader: { ".woff": "file", ".woff2": "file", + ".ttf": "file", }, - plugins: [solidPlugin()], + plugins: [ + sassPlugin({ + quietDeps: ["bulma"], + }), + solidPlugin(), + ], }; } -await Promise.all([ - esbuild.build(buildOpts(["src/entrypoints/admin.ts"])), -]); +await Promise.all([esbuild.build(buildOpts(["src/entrypoints/admin.ts"]))]); diff --git a/registration/frontend/package-lock.json b/registration/frontend/package-lock.json index 205d612f..4a15caeb 100644 --- a/registration/frontend/package-lock.json +++ b/registration/frontend/package-lock.json @@ -5,9 +5,14 @@ "packages": { "": { "dependencies": { + "@fortawesome/fontawesome-free": "^6.6.0", + "@solid-primitives/keyboard": "^1.2.8", "big.js": "^6.2.2", + "bulma": "^1.0.2", + "date-fns": "^4.1.0", "esbuild": "^0.24.0", "esbuild-plugin-solid": "^0.6.0", + "esbuild-sass-plugin": "^3.3.1", "mitt": "^3.0.1", "mqtt": "^5.10.1", "solid-js": "^1.9.3" @@ -460,6 +465,12 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.2.tgz", + "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==", + "peer": true + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", @@ -844,6 +855,14 @@ "node": ">=18" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "engines": { + "node": ">=6" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -892,6 +911,331 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@solid-primitives/event-listener": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/event-listener/-/event-listener-2.3.3.tgz", + "integrity": "sha512-DAJbl+F0wrFW2xmcV8dKMBhk9QLVLuBSW+TR4JmIfTaObxd13PuL7nqaXnaYKDWOYa6otB00qcCUIGbuIhSUgQ==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/keyboard": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@solid-primitives/keyboard/-/keyboard-1.2.8.tgz", + "integrity": "sha512-pJtcbkjozS6L1xvTht9rPpyPpX55nAkfBzbFWdf3y0Suwh6qClTibvvObzKOf7uzQ+8aZRDH4LsoGmbTKXtJjQ==", + "dependencies": { + "@solid-primitives/event-listener": "^2.3.3", + "@solid-primitives/rootless": "^1.4.5", + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/rootless": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.4.5.tgz", + "integrity": "sha512-GFJE9GC3ojx0aUKqAUZmQPyU8fOVMtnVNrkdk2yS4kd17WqVSpXpoTmo9CnOwA+PG7FTzdIkogvfLQSLs4lrww==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/utils": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz", + "integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, "node_modules/@types/big.js": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", @@ -1025,6 +1369,18 @@ "readable-stream": "^4.2.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "optional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.24.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", @@ -1081,12 +1437,23 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-builder": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", + "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", + "peer": true + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "license": "MIT" }, + "node_modules/bulma": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.2.tgz", + "integrity": "sha512-D7GnDuF6seb6HkcnRMM9E739QpEY9chDzzeFrHMyEns/EXyDJuQ0XA0KxbBl/B2NTsKSoDomW61jFGFaAxhK5A==" + }, "node_modules/caniuse-lite": { "version": "1.0.30001677", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", @@ -1107,6 +1474,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/colorjs.io": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", + "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", + "peer": true + }, "node_modules/commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", @@ -1154,6 +1541,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -1171,6 +1567,18 @@ } } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.52", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", @@ -1243,6 +1651,20 @@ "solid-js": ">= 1.0" } }, + "node_modules/esbuild-sass-plugin": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.3.1.tgz", + "integrity": "sha512-SnO1ls+d52n6j8gRRpjexXI8MsHEaumS0IdDHaYM29Y6gakzZYMls6i9ql9+AWMSQk/eryndmUpXEgT34QrX1A==", + "dependencies": { + "resolve": "^1.22.8", + "safe-identifier": "^0.4.2", + "sass": "^1.71.1" + }, + "peerDependencies": { + "esbuild": ">=0.20.1", + "sass-embedded": "^1.71.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1283,6 +1705,26 @@ "node": ">=16.1.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "optional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1301,6 +1743,26 @@ "node": ">=4" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -1333,12 +1795,61 @@ ], "license": "BSD-3-Clause" }, - "node_modules/inherits": { + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "optional": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", @@ -1385,6 +1896,19 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1449,6 +1973,12 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -1477,12 +2007,29 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "optional": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -1514,6 +2061,18 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -1526,18 +2085,430 @@ "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" + }, + "node_modules/sass": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", + "integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.6.tgz", + "integrity": "sha512-Og4aqBnaA3oJfIpHaLuNATAqzBRgUJDYJy2X15V59cot2wYOtiT/ciPnyuq1o7vpDEeOkHhEd+mSviSlXoETug==", + "peer": true, + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "buffer-builder": "^0.2.0", + "colorjs.io": "^0.5.0", + "immutable": "^4.0.0", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "optionalDependencies": { + "sass-embedded-android-arm": "1.80.6", + "sass-embedded-android-arm64": "1.80.6", + "sass-embedded-android-ia32": "1.80.6", + "sass-embedded-android-riscv64": "1.80.6", + "sass-embedded-android-x64": "1.80.6", + "sass-embedded-darwin-arm64": "1.80.6", + "sass-embedded-darwin-x64": "1.80.6", + "sass-embedded-linux-arm": "1.80.6", + "sass-embedded-linux-arm64": "1.80.6", + "sass-embedded-linux-ia32": "1.80.6", + "sass-embedded-linux-musl-arm": "1.80.6", + "sass-embedded-linux-musl-arm64": "1.80.6", + "sass-embedded-linux-musl-ia32": "1.80.6", + "sass-embedded-linux-musl-riscv64": "1.80.6", + "sass-embedded-linux-musl-x64": "1.80.6", + "sass-embedded-linux-riscv64": "1.80.6", + "sass-embedded-linux-x64": "1.80.6", + "sass-embedded-win32-arm64": "1.80.6", + "sass-embedded-win32-ia32": "1.80.6", + "sass-embedded-win32-x64": "1.80.6" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.80.6.tgz", + "integrity": "sha512-UeUKMTRsnz4/dh7IzvhjONxa4/jmVp539CHDd8VZOsqg9M3HcNJNIkUzQWbuwZ+nSlWrTuo7Tvn3XlypopCBzw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.80.6.tgz", + "integrity": "sha512-4rC4ZGM/k4ENVjLXnK3JTst8e8FI9MHSol2Fl7dCdYyJ3KLnlt4qL4AEYfU8zq1tcBb7CBOSZVR+CzCKubnXdg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-ia32": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.80.6.tgz", + "integrity": "sha512-Lxz2SXE2KdHnynuHF+D6flDvrd55/zaEAWUeka9MxEr6FmR66d8UBOIy5ETwCSUd//S/SE5Jl6oTnHppgD1zNA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.80.6.tgz", + "integrity": "sha512-hKdxY/oOqB+JJhSoBTDM5DJO1j/xtxQgayh2cLCCUx37IQQe3SEdc3V2JFf/4mIo5peaS4cjqwwSATF+l2zaXg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.80.6.tgz", + "integrity": "sha512-Eap2Fi3kTx/rVLBsOnOp5RYPr5+lFjTZ652zR24dmYFe9/sDgasakJIOPjOvD2bRuL9z0uWEY1AXVeeOPeZKrg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.80.6.tgz", + "integrity": "sha512-0mnAx8Vq6Gxj3PQt3imgITfK33hhqrSKpyHSuab71gZZni5opsdtoggq2JawW+1taRFTEZwbZJLKZ0MBDbwCCA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.80.6.tgz", + "integrity": "sha512-Ib20yNZFOrJ7YVT+ltoe+JQNKPcRclM3iLAK69XZZYcSeFM/72SCoQBAaVGIpT23dxDp7FXiE4lO602c3xTRwQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.80.6.tgz", + "integrity": "sha512-QR0Q6TZox/ThuU2r9c0s3fKCgU2rXAEocpitdgxFp6tta+GsQlMFV3oON2unAa8Bwnuxkmf0YOaK0Oy/TwzkXw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.80.6.tgz", + "integrity": "sha512-n5r98pBXawrQQKaxIYCMM1zDpnngsqxTkOrmvsYLFiAMCSbR0lWf/7sBB33k/Pm0D6dsbp3jpHilCoQNKI3jIw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-ia32": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.80.6.tgz", + "integrity": "sha512-O6dWZdcOkryRdDCxVMGOeVowgblpDgVcAuRtZ1F1X7XfbpDriTQm64D+9vVZIrywYSPoJfQMJJ662cr0wUs9IQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.80.6.tgz", + "integrity": "sha512-X9FC8s8fvQGRiXc+eATlZ57N44Iq3nNa0M0ugi3ysdJwkaNYvOeS4QzBHKQAaw3QiTqdxTnLUHHVBkyzdCi9pw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.80.6.tgz", + "integrity": "sha512-VeUSHUi3MAsvOlg9QI4X/2j04h1659aE+7qKP/282CYBTrGkjFGSXZhIki9WKWDgIpDiSInRYXfQQRWhPhjCDg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-ia32": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.80.6.tgz", + "integrity": "sha512-GqitS2Nab8ah0+wfCqaxW1hnI1piC08FimL6+lM9YWK5DbCOOF82IapbvJOy0feUmd/wNnHmyNTgE9h0zVMFdQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.80.6.tgz", + "integrity": "sha512-ySs15z7QSRRQK/aByEEqaJLYW/sTpfynefNPZCtsVNVEzNRwy+DRpxNChtxo+QjKq97ocXETbdG5KLik7QOTJg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.80.6.tgz", + "integrity": "sha512-DzeNqU/SN0mWFznoOH4RtVGcrg3Eoa41pUQhKMtrhNbCmIE1zNDunUiAEVTNpdHJF4nxf7ELUPXWmStM31CbUQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.80.6.tgz", + "integrity": "sha512-AyoHJ3icV9xuJjq1YzJqpEj2XfiC/KBkVYTUrCELKiXP0DN1gi/BpUwZNCAgCM3CyEdMef4LQM/ztCYJxYzdyg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.80.6.tgz", + "integrity": "sha512-EohsE9CEqx0ycylnsEj/0DNPG99Tb0qAVZspiAs5xHFCJjXOFfp3cRQu0BRf+lZ1b72IhPFXymzVtojvzUHb7g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.80.6.tgz", + "integrity": "sha512-29wETQi1ykeVvpd4zMVokpQKFSOZskGJzZawuuNCdo7BHjHKIRDsqbz8YT1CewHPBshI0hfD21fenmjxYjGXPQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-ia32": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.80.6.tgz", + "integrity": "sha512-1s3OpK2iTIfIL/a91QhAQnffsbuWfnsM8Lx4Fxt0f7ErnxjCV6q8MUFTV/UhcLtLyTFnPCA62DLjp2KGCjMI9A==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.80.6", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.80.6.tgz", + "integrity": "sha512-0pH4Zr9silHkcmLPC0ghnD3DI0vMsjA7dKvGR32/RbbjOSvHV5cDQRLiuVJAPp34dfMA7kJd1ysSchRdH0igAQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1579,6 +2550,14 @@ "seroval-plugins": "^1.1.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -1617,6 +2596,44 @@ ], "license": "MIT" }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1677,6 +2694,12 @@ "integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==", "license": "ISC" }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "peer": true + }, "node_modules/worker-timers": { "version": "7.1.8", "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", diff --git a/registration/frontend/package.json b/registration/frontend/package.json index 4a1745ac..f10a6f5a 100644 --- a/registration/frontend/package.json +++ b/registration/frontend/package.json @@ -1,8 +1,13 @@ { "dependencies": { + "@fortawesome/fontawesome-free": "^6.6.0", + "@solid-primitives/keyboard": "^1.2.8", "big.js": "^6.2.2", + "bulma": "^1.0.2", + "date-fns": "^4.1.0", "esbuild": "^0.24.0", "esbuild-plugin-solid": "^0.6.0", + "esbuild-sass-plugin": "^3.3.1", "mitt": "^3.0.1", "mqtt": "^5.10.1", "solid-js": "^1.9.3" diff --git a/registration/frontend/src/admin/attendee-search.tsx b/registration/frontend/src/admin/attendee-search.tsx new file mode 100644 index 00000000..9dff70ca --- /dev/null +++ b/registration/frontend/src/admin/attendee-search.tsx @@ -0,0 +1,265 @@ +import { + Accessor, + Component, + createEffect, + createResource, + createSignal, + For, + Setter, + Show, + useContext, +} from "solid-js"; + +import { ConfigContext } from "./providers/config-provider"; +import { ApisUrls, CSRF_TOKEN } from "../entrypoints/admin"; +import { CartManager } from "./cart/cart-manager"; + +interface BadgeResult { + id: number; + edit_url: string; + attendee: Attendee; + badgeName: string; + abandoned: string; +} + +interface Attendee { + firstName: string; + lastName: string; + preferredName?: string; +} + +async function getSearchResults( + urls: ApisUrls, + query: string +): Promise { + // Clear results if we search for an empty string. + if (query.trim().length === 0) { + return []; + } + + let formData = new FormData(); + formData.set("search", query); + + const resp = await fetch(urls.onsite_admin_search, { + method: "POST", + body: formData, + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + return data["results"]; +} + +const BadgeTableRow: Component<{ + cartManager: CartManager; + badge: BadgeResult; +}> = (props) => { + return ( + + +
+ {`${props.badge.attendee.firstName} ${props.badge.attendee.lastName}`} +
+ + +
+ Preferred Name: + {props.badge.attendee.preferredName} +
+
+ + {props.badge.badgeName} + {props.badge.abandoned} + +
+ + + + + + + +
+ + + ); +}; + +const BadgeTableLoader: Component = () => { + return ( + + +
+
Full Name
+
+ + +
+
Badge Name
+
+ + +
+
Status
+
+ + +
+ + + + + + + +
+ + + ); +}; + +export const AttendeeSearch: Component<{ + cartManager: CartManager; + searchQuery: Accessor; + setSearchQuery: Setter; +}> = (props) => { + const config = useContext(ConfigContext); + + const [results, { refetch }] = createResource( + props.searchQuery, + async (query) => getSearchResults(config.urls, query) + ); + + let searchInputRef: HTMLInputElement; + + createEffect(() => { + console.log(`Updating search query: ${props.searchQuery()}`); + + if (props.searchQuery()) { + searchInputRef.value = props.searchQuery(); + } + }); + + return ( +
+
+
+
+
+
Attendee Search
+ + +
+
+ +
+
{ + ev.preventDefault(); + props.setSearchQuery(searchInputRef.value); + }} + > +
+

+ { + if (ev.target.value.length === 0) { + props.setSearchQuery(""); + } + }} + /> +

+ +

+ +

+
+
+
+ +
+ + + + + + + + + + + }> + 0} + fallback={ + + + + } + > + + {(badge, index) => ( + + )} + + + + +
Legal NameBadge NameStatus
No results.
+
+
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/cart/cart-manager.ts b/registration/frontend/src/admin/cart/cart-manager.ts index f688a7a2..24651e37 100644 --- a/registration/frontend/src/admin/cart/cart-manager.ts +++ b/registration/frontend/src/admin/cart/cart-manager.ts @@ -1,11 +1,7 @@ import { Setter } from "solid-js"; -import { ApisUrls } from "../../entrypoints/admin"; -import emitter, { publishMessage } from "../mqtt"; - -const CSRF_TOKEN = document.querySelector( - "meta[name='csrf_token']" -).content; +import { ApisUrls, CSRF_TOKEN } from "../../entrypoints/admin"; +import emitter from "../mqtt"; export class CartManager { urls: ApisUrls; @@ -15,11 +11,13 @@ export class CartManager { this.urls = urls; this.setCartEntries = setCartEntries; - // Only one CartManager should be active at a time. - emitter.off("refresh"); emitter.on("refresh", this.refreshCart.bind(this)); } + close() { + emitter.off("refresh", this.refreshCart.bind(this)); + } + private updateCart(data: CartResponse) { if (!data.success) { alert("Failed to update cart"); @@ -30,7 +28,7 @@ export class CartManager { this.setCartEntries(data); } - public async addCartId(id: string) { + public async addCartId(id: number) { let url = new URL(this.urls.onsite_add_to_cart, window.location.href); url.search = new URLSearchParams({ id: id.toString() }).toString(); @@ -121,7 +119,9 @@ export class CartManager { return data; } - public async printBadges(ids: number[]): Promise { + public async printBadges( + ids: number[] + ): Promise { const assignResp = await fetch(this.urls.assign_badge_number, { method: "POST", headers: { @@ -141,10 +141,7 @@ export class CartManager { return assignData; } - let url = new URL( - this.urls.onsite_print_badges, - window.location.href - ); + let url = new URL(this.urls.onsite_print_badges, window.location.href); let params = new URLSearchParams(); ids.forEach((id) => params.append("id", id.toString())); url.search = params.toString(); diff --git a/registration/frontend/src/admin/cart/components/Cart.tsx b/registration/frontend/src/admin/cart/components/Cart.tsx index b8b153af..5e3566be 100644 --- a/registration/frontend/src/admin/cart/components/Cart.tsx +++ b/registration/frontend/src/admin/cart/components/Cart.tsx @@ -1,5 +1,5 @@ import { Show } from "solid-js/web"; -import { Component, createEffect } from "solid-js"; +import { Component, createEffect, createResource } from "solid-js"; import { CartManager, CartResponse } from "../cart-manager"; import { CartEntries } from "./CartEntries"; @@ -9,47 +9,65 @@ export const Cart: Component<{ cartManager: CartManager; cartEntries: CartResponse; }> = (props) => { + const [refresh, { refetch: refreshCart }] = createResource(async () => { + await props.cartManager.refreshCart(); + }); + + const [clear, { refetch: clearCart }] = createResource(async () => { + await props.cartManager.clearCart(); + }); + + const anythingLoading = () => refresh.loading || clear.loading; + createEffect(() => { - props.cartManager.refreshCart(); + refreshCart(); }); return ( -
-
- -
- +
+
+
+
Cart
+ +
+
+ + + +
+
+
+ +
+ - -
); }; diff --git a/registration/frontend/src/admin/cart/components/CartActions.tsx b/registration/frontend/src/admin/cart/components/CartActions.tsx index 812c4cdb..f83110da 100644 --- a/registration/frontend/src/admin/cart/components/CartActions.tsx +++ b/registration/frontend/src/admin/cart/components/CartActions.tsx @@ -1,4 +1,12 @@ -import { Component, createEffect, createMemo, useContext } from "solid-js"; +import { + Component, + createEffect, + createMemo, + createSignal, + JSX, + Setter, + useContext, +} from "solid-js"; import { Big } from "big.js"; import { BadgePrintResponse, CartManager, CartResponse } from "../cart-manager"; @@ -7,6 +15,19 @@ import { publishMessage } from "../../mqtt"; const PRINTABLE_STATUS = new Set(["Paid", "Comp", "Staff", "Dealer"]); +type ActionButton = "cash" | "card" | "print"; + +async function trackLoadingButton( + setLoadingButton: Setter, + button: ActionButton, + action: Promise +): Promise { + setLoadingButton(button); + const resp = await action; + setLoadingButton(null); + return resp; +} + async function attemptCashPayment( manager: CartManager, reference: string, @@ -67,10 +88,13 @@ async function printBadges( let url = new URL(pdfUrl, window.location.href); url.searchParams.append("file", data.file); - publishMessage("action", JSON.stringify({ - action: "print", - url, - })); + publishMessage( + "action", + JSON.stringify({ + action: "print", + url, + }) + ); } else { window.open(data.url, "badge"); } @@ -82,13 +106,21 @@ export const CartActions: Component<{ }> = (props) => { const config = useContext(ConfigContext); + const [loadingButton, setLoadingButton] = createSignal(); + const hasHold = createMemo( () => props.cartEntries?.result?.some((entry) => !!entry.holdType) || isNaN(parseFloat(props?.cartEntries?.total)) ); - const needsPayment = () => parseFloat(props.cartEntries?.total) > 0; + const needsPayment = createMemo( + () => + parseFloat(props.cartEntries?.total) > 0 && + props.cartEntries?.result.some( + (entry) => !PRINTABLE_STATUS.has(entry.abandoned) + ) + ); const printableBadgeIds = createMemo( () => @@ -104,64 +136,106 @@ export const CartActions: Component<{ ?.map((badge) => badge.id) || [] ); - createEffect(() => - console.log( - `hold: ${hasHold()}, needsPayment: ${needsPayment()}, printable: ${printableBadgeIds()}` - ) - ); - const canTenderCash = () => config.permissions.cash && !hasHold() && needsPayment(); const canUseCard = () => !hasHold() && needsPayment(); const hasPrintableBadges = () => printableBadgeIds()?.length > 0 || false; - const canApplyDiscount = () => config.permissions.discount && false; return ( -
-
-
- -
-
- -
+
+
+ + attemptCashPayment( + props.manager, + props.cartEntries.reference, + props.cartEntries.total + ) + } + > + + + + + Tender Cash + + + enableCardPayment(props.manager)} + > + + + + + Credit/Debit Card + +
-
-
- -
+
+ + printBadges( + props.manager, + printableBadgeIds(), + config.mqtt.supports_printing && !ev.shiftKey, + config.urls.pdf + ) + } + > + + + + + Print Badges + +
); }; + +const LoadableButton: Component<{ + button: ActionButton; + class: string; + disabled: boolean; + loadingButton: ActionButton | null; + setLoadingButton: Setter; + action: (ev: MouseEvent) => Promise; + children: JSX.Element; +}> = (props) => { + let classes = `button is-fullwidth ${props.class}`; + + return ( +
+ +
+ ); +}; diff --git a/registration/frontend/src/admin/cart/components/CartBadge.tsx b/registration/frontend/src/admin/cart/components/CartBadge.tsx index 45947b85..e4736fa9 100644 --- a/registration/frontend/src/admin/cart/components/CartBadge.tsx +++ b/registration/frontend/src/admin/cart/components/CartBadge.tsx @@ -1,74 +1,86 @@ import { Component, Show } from "solid-js"; import { Badge, CartManager } from "../cart-manager"; +import { cleanMoneyAmount } from "./CartEntries"; export const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( props ) => { return ( -
-
- - {props.badge.abandoned} - +
+
+
+
+ + {props.badge.abandoned} + - - {props.badge.holdType} - + + {props.badge.holdType} + - - - - - + + + + + + + - - {props.badge.badgeNumber} - + + {props.badge.badgeNumber} + +
- - {`${props.badge.firstName} ${props.badge.lastName}`} - + - { - ev.preventDefault(); - props.manager.removeBadge(props.badge.id); - }} - > - - +
+ + MINOR FORM REQUIRED + +
- - MINOR FORM REQUIRED - -
+
+ +
+
- - - - - - - - - - - - - - - -
Badge NameLevelPrice
{props.badge.badgeName}{props.badge.effectiveLevel?.name || ""}{props.badge.effectiveLevel?.price || "0.00"}
+
+ + + + + + + + + + + + + + + +
Badge NameLevelPrice
{props.badge.badgeName}{props.badge.effectiveLevel?.name || ""}{cleanMoneyAmount(props.badge.effectiveLevel?.price)}
+
+
); }; diff --git a/registration/frontend/src/admin/cart/components/CartEntries.tsx b/registration/frontend/src/admin/cart/components/CartEntries.tsx index 7bfa3fe0..c3dc579e 100644 --- a/registration/frontend/src/admin/cart/components/CartEntries.tsx +++ b/registration/frontend/src/admin/cart/components/CartEntries.tsx @@ -1,8 +1,23 @@ import { Component, createMemo, For, Show } from "solid-js"; +import { Big } from "big.js"; import { CartManager, CartResponse } from "../cart-manager"; import { CartBadge } from "./CartBadge"; +export function cleanMoneyAmount(input: string): string { + if (input == "?") return "0.00"; + + let parsed: Big; + try { + parsed = new Big(input); + } catch (err) { + console.error(`Could not parse money: ${err}`); + return input; + } + + return `$${parsed.toFixed(2)}`; +} + export const CartEntries: Component<{ manager: CartManager; cart: CartResponse; @@ -15,8 +30,8 @@ export const CartEntries: Component<{ options.push({ quantity: 1, item: `Discount ${result.discount.name}`, - price: `-${result.discount.amount_off} / ${result.discount.percent_off}%`, - total: `-$${result.level_discount}`, + price: `-${cleanMoneyAmount(result.discount.amount_off)} / ${result.discount.percent_off}%`, + total: `-${cleanMoneyAmount(result.level_discount)}`, }); } return options; @@ -26,52 +41,40 @@ export const CartEntries: Component<{ return ( <> -
- +
+
- + - + - + - + - - + +
Subtotal:{props.cart.subtotal}{cleanMoneyAmount(props.cart.subtotal)}
Discounts:{props.cart.total_discount}{cleanMoneyAmount(props.cart.total_discount)}
Donation to Charity:{props.cart.charityDonation}{cleanMoneyAmount(props.cart.charityDonation)}
Donation to Convention:{props.cart.orgDonation}{cleanMoneyAmount(props.cart.orgDonation)}
- Total: - {`${props.cart.total}`}Total:{cleanMoneyAmount(props.cart.total)}
- - {(badge, index) => ( - - )} - - 0}> -
- +
+
- + @@ -89,6 +92,20 @@ export const CartEntries: Component<{
Order ItemPricePrice
+ + 0}> +
+ + {(badge, index) => ( + + )} + +
+
); }; diff --git a/registration/frontend/src/admin/cart/index.tsx b/registration/frontend/src/admin/cart/index.tsx deleted file mode 100644 index 3d4ba72d..00000000 --- a/registration/frontend/src/admin/cart/index.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createSignal } from "solid-js"; -import { render } from "solid-js/web"; - -import { CartManager, CartResponse } from "./cart-manager"; -import { ApisConfig } from "../../entrypoints/admin"; -import { Cart } from "./components/Cart"; -import { ConfigContext } from "../providers/config-provider"; - -const [cartEntries, setCartEntries] = createSignal(null); - -export let cartManager: CartManager | null = null; - -export default function createCart(config: ApisConfig) { - cartManager = new CartManager(config.urls, setCartEntries); - window["cartManager"] = cartManager; - - render(() => { - return ( - - - - ); - }, document.getElementById("cart")); -} diff --git a/registration/frontend/src/admin/index.scss b/registration/frontend/src/admin/index.scss new file mode 100644 index 00000000..6bada086 --- /dev/null +++ b/registration/frontend/src/admin/index.scss @@ -0,0 +1,14 @@ +@use "bulma/sass"; + +@use "@fortawesome/fontawesome-free/scss/fontawesome.scss"; +@use "@fortawesome/fontawesome-free/scss/solid.scss" with ( + $fa-font-path: "@fortawesome/fontawesome-free/webfonts" +); + +.d-block { + display: block; +} + +.has-text-nowrap { + text-wrap: nowrap; +} diff --git a/registration/frontend/src/admin/navbar.tsx b/registration/frontend/src/admin/navbar.tsx new file mode 100644 index 00000000..940d1208 --- /dev/null +++ b/registration/frontend/src/admin/navbar.tsx @@ -0,0 +1,246 @@ +import { render } from "solid-js/web"; +import { Component, For, Show, useContext } from "solid-js"; +import { Big } from "big.js"; +import { createShortcut, KbdKey } from "@solid-primitives/keyboard"; + +import { ApisConfig, CSRF_TOKEN } from "../entrypoints/admin"; +import { ConfigContext } from "./providers/config-provider"; + +const ActionButton: Component<{ + name: string; + icon: string; + action: () => any; + keyboardShortcut?: KbdKey[]; +}> = (props) => { + if (props.keyboardShortcut) { + createShortcut(props.keyboardShortcut, props.action, { + preventDefault: true, + }); + } + + return ( + { + ev.preventDefault(); + props.action(); + }} + > + + + + {props.name} + + ); +}; + +async function makeSimpleRequest(url: string) { + const resp = await fetch(url, { + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + return data; +} + +async function amountRequest(url: string, message: string) { + const input = prompt(message); + let amount: Big; + try { + amount = new Big(input); + } catch (err) { + alert("Invalid input."); + return; + } + + let formData = new FormData(); + formData.set("amount", amount.toString()); + + const resp = await fetch(url, { + method: "POST", + body: formData, + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + if (data["success"]) { + alert("Success!"); + } else { + alert(`Error: ${data.message}`); + } +} + +const Actions: Component<{ config: ApisConfig }> = (props) => { + return ( + + ); +}; + +const Navbar: Component = () => { + const config = useContext(ConfigContext); + + return ( + + ); +}; + +export default function createActions(config: ApisConfig) { + render(() => { + return ( + + + + ); + }, document.getElementById("admin-navbar")); +} diff --git a/registration/frontend/src/admin/onsite.tsx b/registration/frontend/src/admin/onsite.tsx new file mode 100644 index 00000000..bb95f385 --- /dev/null +++ b/registration/frontend/src/admin/onsite.tsx @@ -0,0 +1,51 @@ +import { Component, createSignal, onCleanup, useContext } from "solid-js"; +import { render } from "solid-js/web"; + +import { ApisConfig } from "../entrypoints/admin"; +import { ConfigContext } from "./providers/config-provider"; +import { AttendeeSearch } from "./attendee-search"; +import { CartManager, CartResponse } from "./cart/cart-manager"; +import { Cart } from "./cart/components/Cart"; +import { ScanPanel } from "./scan-actions"; + +const Onsite: Component = () => { + const config = useContext(ConfigContext); + + const [cartEntries, setCartEntries] = createSignal(); + const cartManager = new CartManager(config.urls, setCartEntries); + + onCleanup(() => { + cartManager.close(); + }); + + const [searchQuery, setSearchQuery] = createSignal(); + + return ( +
+
+ + + setSearchQuery(name)} /> +
+ +
+ +
+
+ ); +}; + +export default function createOnsiteExperience(config: ApisConfig) { + render( + () => ( + + + + ), + document.getElementById("onsite") + ); +} diff --git a/registration/frontend/src/admin/scan-actions.tsx b/registration/frontend/src/admin/scan-actions.tsx index a39a3d41..f8228e07 100644 --- a/registration/frontend/src/admin/scan-actions.tsx +++ b/registration/frontend/src/admin/scan-actions.tsx @@ -1,13 +1,29 @@ -import { Component, createSignal, For, Show } from "solid-js"; +import { + Component, + createEffect, + createMemo, + createSignal, + For, + JSX, + Show, + useContext, +} from "solid-js"; + import emitter from "./mqtt"; -import { render } from "solid-js/web"; +import { differenceInYears } from "date-fns"; +import { ConfigContext } from "./providers/config-provider"; interface IdData { - expirationDate: string; - dateOfBirth: string; - age: number; - givenName: string; - familyName: string; + first: string; + last: string; + dob: string; + expiry: string; + address: string; + address2: string; + city: string; + state: string; + ZIP: string; + country: string; } interface ShcData { @@ -42,112 +58,226 @@ interface ShcMatch { dob: boolean; } -const scanPanel = document.getElementById("scan-panel"); const [scanLog, setScanLog] = createSignal({ url: undefined, id: undefined, shc: undefined, }); -function ScanPanel() { - const shcMatch = () => { +export const ScanPanel: Component<{ + gotScannedName(name: string): void; +}> = (props) => { + const shcMatch = createMemo(() => { const data = scanLog(); - if (!data.id || !data.shc) return { name: false, dob: false }; + if (!data.id || !data.shc) return { name: true, dob: true }; - const idName = `${data.id.givenName} ${data.id.familyName}`; + const idName = `${data.id.first} ${data.id.last}`; const name = idName.localeCompare(data.shc.name) === 0; - const dob = data.id.dateOfBirth === data.shc.birthday; + const dob = data.id.dob === data.shc.birthday; return { name, dob }; - }; + }); + + createEffect(() => { + const name = scanLog().id?.last; + if (name) { + props.gotScannedName(name); + } + }); + + const hasAnyScans = () => scanLog().id || scanLog().shc || scanLog().url; return ( - <> -
- Scanner History - -
+
+
+
+
+
Scanner Entries
+ +
+ +
+
+
+ + +
No items scanned.
+
-
- +
+ +
- +
+ +
- +
+ +
- +
); -} +}; const CloseButton: Component<{ close(): any }> = (props) => { return ( - + + ); +}; + +const MismatchedData: Component<{ + matched: boolean; + message: string; + children: JSX.Element; +}> = (props) => { + return ( + + + + + + {props.children} + + + ); +}; + +const NameBirthday: Component<{ + name: string; + birthday: string; + shcMatch?: ShcMatch; +}> = (props) => { + const age = () => { + return differenceInYears(new Date(), props.birthday); + }; + + return ( +
+
+ + {props.name} + +
+
+ + + + + + {`${props.birthday} (${age()} years)`} + + +
+
); }; const UrlEntry: Component<{ url: string }> = (props) => { return ( -
-
- - {props.url} - +
+ + + ); }; const IdEntry: Component<{ data: IdData }> = (props) => { - const expirationDate = new Date(props.data.expirationDate); - const expired = new Date() > expirationDate; + const config = useContext(ConfigContext); - const panelClasses = { - "panel-warning": expired, - "panel-primary": !expired, + const expirationDate = () => new Date(props.data.expiry); + const expired = () => new Date() > expirationDate(); + + const panelClasses = () => { + return { + "is-warning": expired(), + "is-success": !expired(), + }; }; + const regUrl = createMemo(() => { + let url = new URL(config.urls.onsite, window.location.href); + url.searchParams.set("firstName", props.data.first); + url.searchParams.set("lastName", props.data.last); + url.searchParams.set("dob", props.data.dob); + url.searchParams.set("address1", props.data.address); + if (props.data.address2) + url.searchParams.set("address2", props.data.address2); + url.searchParams.set("city", props.data.city); + url.searchParams.set("state", props.data.state); + url.searchParams.set("postalCode", props.data.ZIP.substring(0, 5)); + return url.toString(); + }); + return ( -
-
- - License Scanned - - +
+
+ + + + + ID Card + + + + - {` Expired ${props.data.expirationDate}`} + {`Expired ${props.data.expiry}`} + setScanLog({ ...scanLog(), id: undefined })} />
-
- {`${props.data.givenName} ${props.data.familyName}`} - - - {` ${props.data.dateOfBirth} (${props.data.age} years)`} - + + -
+
); }; @@ -163,36 +293,39 @@ const ShcEntry: Component<{ data: ShcData; shcMatch: ShcMatch }> = (props) => { }; return ( -
-
- Vaccination Record - {status()} +
+ + + + + Vaccination Record - {status()} + + setScanLog({ ...scanLog(), shc: undefined })} />
-
- {props.data.name}} - > - - - {props.data.name} - - +
+ - +
- + - + @@ -200,9 +333,9 @@ const ShcEntry: Component<{ data: ShcData; shcMatch: ShcMatch }> = (props) => { {(vaccine, index) => { return ( - + - + ); }} @@ -210,7 +343,7 @@ const ShcEntry: Component<{ data: ShcData; shcMatch: ShcMatch }> = (props) => {
DateDate VaccineLotLot
{vaccine.date}{vaccine.date} {vaccine.name}{vaccine.lotNumber}{vaccine.lotNumber}
-
+ ); }; @@ -221,7 +354,7 @@ emitter.on("open", (payload) => { return; } - window.open(url, "_blank"); + window.open(url, "link"); setScanLog({ ...scanLog(), url, @@ -251,5 +384,3 @@ emitter.on("scan/shc", (payload: ShcData) => { shc: payload, }); }); - -render(ScanPanel, scanPanel); diff --git a/registration/frontend/src/entrypoints/admin.ts b/registration/frontend/src/entrypoints/admin.ts index 818962d0..9384a8c3 100644 --- a/registration/frontend/src/entrypoints/admin.ts +++ b/registration/frontend/src/entrypoints/admin.ts @@ -1,6 +1,13 @@ -import { connectToMqtt } from "../admin/mqtt"; import "../admin/scan-actions"; -import createCart from "../admin/cart"; +import { connectToMqtt } from "../admin/mqtt"; +import createActions from "../admin/navbar"; +import createOnsiteExperience from "../admin/onsite"; + +import "../admin/index.scss"; + +export const CSRF_TOKEN = document.querySelector( + "meta[name='csrf_token']" +).content; export interface ApisConfig { debug: boolean; @@ -8,6 +15,7 @@ export interface ApisConfig { mqtt: ApisMqttConfig; urls: ApisUrls; permissions: ApisPermissions; + terminals: ApisTerminalSettings; } export interface ApisMqttConfig { @@ -24,22 +32,44 @@ export interface ApisMqttAuth { export interface ApisUrls { assign_badge_number: string; + cash_deposit: string; + cash_pickup: string; + close_drawer: string; + close_terminal: string; complete_cash_transaction: string; enable_payment: string; + no_sale: string; onsite_add_to_cart: string; onsite_admin_cart: string; onsite_admin_clear_cart: string; + onsite_admin_search: string; onsite_print_badges: string; onsite_remove_from_cart: string; - registration_badge_change: string; + onsite: string; + open_drawer: string; + open_terminal: string; pdf: string; + ready_terminal: string; + registration_badge_change: string; + safe_drop: string; } export interface ApisPermissions { cash: boolean; + cash_admin: boolean; discount: boolean; } +export interface ApisTerminalSettings { + selected?: number; + available: ApisTerminal[]; +} + +export interface ApisTerminal { + id: number; + name: string; +} + declare global { const APIS_CONFIG: ApisConfig; } @@ -48,4 +78,5 @@ if (APIS_CONFIG.mqtt) { connectToMqtt(APIS_CONFIG.mqtt) } -createCart(APIS_CONFIG); +createActions(APIS_CONFIG); +createOnsiteExperience(APIS_CONFIG); diff --git a/registration/templates/registration/master_admin.html b/registration/templates/registration/master_admin.html index 141abbb5..93d8d1f9 100644 --- a/registration/templates/registration/master_admin.html +++ b/registration/templates/registration/master_admin.html @@ -1,17 +1,17 @@ - {% load static settings %} - + + + - APIS Register + - - - - - + APIS Register + + + {% block head %}{% endblock %} - - - -
- - {% block content %} - - {% endblock %} +
+ {% block content %}{% endblock %}
- - - - - - - - - - -{% block javascript %} +{% block javascript %}{% endblock %} -{% endblock %} + diff --git a/registration/templates/registration/onsite-admin.html b/registration/templates/registration/onsite-admin.html index f3b76d2f..71467eb0 100644 --- a/registration/templates/registration/onsite-admin.html +++ b/registration/templates/registration/onsite-admin.html @@ -1,422 +1,21 @@ {% extends "registration/master_admin.html" %} -{% load admin_urls static settings %} -{% block head %} - -{% endblock %} -{% block content %} - - -
- -
-
- - -
-
-
-
-
- -
-
- -
- -
-
-
- -
- - {% for error in errors %} - - {% endfor %} -
- - Error: - There was a problem while connecting to the server. - Reload -
+{% load static %} -
-
-
-
-
- - - - - Add Attendee - -
-
-
-
-
-
Search results
- -
- - - - - - - - - - - - - - - {% for badge in results %} - - - - - - - {% endfor %} - -
Legal NameBadge NameStatusAction
- {{ badge.attendee.firstName }} {{ badge.attendee.lastName }}
- {% if badge.attendee.preferredName %} - Preferred Name: {{ badge.attendee.preferredName }} - {% endif %} -
{{ badge.badgeName }}{{ badge.abandoned }} - - -
-
-
-
- -
-
-
-
- -
-
-

Cart  - - Refresh - -

-
-
- Clear -
-
-
- -
-
- {% if perms.registration.cash %} - - {% endif %} -
-
- -
-
-
-
- -
- -
- -
-
- - {% if perms.registration.discount %} -
-
- -
- -
- -
-
- {% else %} -
-
- -
-
- {% endif %} - -
- -
-
- - - - - - - - - - - {% verbatim %} - +{% endblock %} - +{% block content %} +
- - {% endverbatim %} -{% endblock %} - -{% block javascript %} - + {% endfor %} - +
{% endblock %} diff --git a/registration/views/onsite_admin.py b/registration/views/onsite_admin.py index a58cbb41..a15ce6b1 100644 --- a/registration/views/onsite_admin.py +++ b/registration/views/onsite_admin.py @@ -61,10 +61,8 @@ def onsite_admin(request): # Modify a dummy session variable to keep it alive request.session["heartbeat"] = time.time() - event = Event.objects.get(default=True) terminals = list(Firebase.objects.all()) term = request.session.get("terminal", None) - query = request.GET.get("search", None) errors = [] results = None @@ -103,22 +101,6 @@ def onsite_admin(request): # weren't passed an integer errors.append({"type": "danger", "text": "Invalid terminal specified"}) - if query is not None: - results = Badge.objects.filter( - Q(attendee__lastName__icontains=query) - | Q(attendee__preferredName__icontains=query) - | Q(attendee__firstName__icontains=query), - Q(event=event), - ) - if len(results) == 0: - errors.append( - {"type": "warning", "text": 'No results for query "{0}"'.format(query)} - ) - - cart = request.session.get("cart", None) - if cart and len(results) == 1 and results[0].id not in cart: - onsite_add_id_to_cart(request, results[0].id) - terminal = get_active_terminal(request) mqtt_auth = None if terminal: @@ -145,20 +127,36 @@ def onsite_admin(request): }, "urls": { "assign_badge_number": reverse("registration:assign_badge_number"), - "onsite_print_badges": reverse("registration:onsite_print_badges"), + "cash_deposit": reverse("registration:cash_deposit"), + "cash_pickup": reverse("registration:cash_pickup"), + "close_drawer": reverse("registration:close_drawer"), + "close_terminal": reverse("registration:close_terminal"), "complete_cash_transaction": reverse("registration:complete_cash_transaction"), "enable_payment": reverse("registration:enable_payment"), - "onsite_admin_clear_cart": reverse("registration:onsite_admin_clear_cart"), + "no_sale": reverse("registration:no_sale"), "onsite_add_to_cart": reverse("registration:onsite_add_to_cart"), "onsite_admin_cart": reverse("registration:onsite_admin_cart"), + "onsite_admin_clear_cart": reverse("registration:onsite_admin_clear_cart"), + "onsite_admin_search": reverse("registration:onsite_admin_search"), + "onsite_print_badges": reverse("registration:onsite_print_badges"), "onsite_remove_from_cart": reverse("registration:onsite_remove_from_cart"), - "registration_badge_change": reverse("admin:registration_badge_change", args=(0,)), + "onsite": reverse("registration:onsite"), + "open_drawer": reverse("registration:open_drawer"), + "open_terminal": reverse("registration:open_terminal"), "pdf": reverse("registration:pdf"), + "ready_terminal": reverse("registration:ready_terminal"), + "registration_badge_change": reverse("admin:registration_badge_change", args=(0,)), + "safe_drop": reverse("registration:safe_drop"), }, "permissions": { "cash": request.user.has_perm("registration.cash"), + "cash_admin": request.user.has_perm("registration.cash_admin"), "discount": request.user.has_perm("registration.discount"), - } + }, + "terminals": { + "selected": terminal.id if terminal else None, + "available": [{"id": terminal.id, "name": terminal.name} for terminal in terminals], + }, }), } @@ -168,25 +166,32 @@ def onsite_admin(request): @staff_member_required def onsite_admin_search(request): event = Event.objects.get(default=True) - terminals = list(Firebase.objects.all()) query = request.POST.get("search", None) if query is None: return redirect("registration:onsite_admin") - errors = [] results = Badge.objects.filter( Q(attendee__lastName__icontains=query) | Q(attendee__preferredName__icontains=query) | Q(attendee__firstName__icontains=query), Q(event=event), ).prefetch_related("attendee", "event") - if len(results) == 0: - errors = [ - {"type": "warning", "text": 'No results for query "{0}"'.format(query)} - ] - context = {"terminals": terminals, "errors": errors, "results": results} - return render(request, "registration/onsite-admin.html", context) + data = [] + for result in results: + data.append({ + "id": result.id, + "edit_url": reverse("admin:registration_badge_change", args=(result.id,)), + "attendee": { + "firstName": result.attendee.firstName, + "lastName": result.attendee.lastName, + "preferredName": result.attendee.preferredName, + }, + "badgeName": result.badgeName, + "abandoned": result.abandoned, + }) + + return JsonResponse({"success": True, "results": data}) @staff_member_required @@ -948,6 +953,13 @@ def onsite_remove_from_cart(request): {"success": False, "reason": "Need ID parameter"}, status=400 ) + try: + id = int(id) + except ValueError: + return JsonResponse( + {"success": False, "reason": "ID must be integer"}, status=400 + ) + cart = request.session.get("cart", None) if cart is None: return JsonResponse({"success": False, "reason": "Cart is empty"}) From dd23cc1c2dc9b177ec0be2b0036ba5218d9e7285 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Thu, 7 Nov 2024 23:27:28 -0500 Subject: [PATCH 05/44] Remove now unused admin files. --- registration/static/js/onsite/onsite_admin.js | 519 ------------------ registration/static/js/onsite/onsite_push.js | 194 ------- 2 files changed, 713 deletions(-) delete mode 100644 registration/static/js/onsite/onsite_admin.js delete mode 100644 registration/static/js/onsite/onsite_push.js diff --git a/registration/static/js/onsite/onsite_admin.js b/registration/static/js/onsite/onsite_admin.js deleted file mode 100644 index fd9341ee..00000000 --- a/registration/static/js/onsite/onsite_admin.js +++ /dev/null @@ -1,519 +0,0 @@ -let refresh_cart; -let cartData = []; -let cartTemplateData = []; - -let get_printable = function () { - // Anything in the cart marked as paid is eligible for printing - let printQueue = []; - let skipped = []; - $.each(cartData.result, function(key, value) { - if (value.printed) { - skipped.push(value); - } - - if (((value.abandoned.toUpperCase() === "PAID") || - (value.abandoned.toUpperCase() === "COMP") || - (value.abandoned.toUpperCase() === "STAFF") || - (value.abandoned.toUpperCase() === "DEALER")) && - (value.holdType === null) && - (value.printed === false)) { - printQueue.push(value); - } - }); - - if (printQueue.length > 0) { - $("#print_button").removeAttr("disabled"); - } else { - $("#print_button").attr("disabled", "disabled"); - } - - return printQueue; -}; - -let print_badges = function(e) { - let printQueue = get_printable(); - - // assign badge numbers - $.ajax(URL_REGISTRATION_ASSIGN_BADGE_NUMBER, { - data : JSON.stringify(printQueue), - contentType : 'application/json', - type : 'POST' - }) - .done(function(data) { - console.log(data.success); - }).success(function (data) { - let printIDs = []; - $.each(printQueue, function(idx, badge) { - printIDs.push(badge.id); - }); - // print badges - - $.getJSON(URL_REGISTRATION_ONSITE_PRINT_BADGES + "?id=" + printIDs.join("&id="), function (data) { - if (!data.success) { - alert("Error while printing badges"); - } - window.open(data.url); - }).fail(function (data) { - $("#cart-error").html("Server error while assigning badge numbers:
"+data.message).fadeIn(); - }); - - // clear cart - $.getJSON(URL_REGISTRATION_ONSITE_ADMIN_CLEAR_CART); - refresh_cart(); - }); -}; - -$(document).ready(function () { - $.addTemplateFormatter("MinorFormFormatter", - function(value, template) { - if (parseInt(value) < 18) { - return 'MINOR FORM REQUIRED'; - } - return "18+"; - }); - - $.addTemplateFormatter("PaidBadgeFormatter", - function(value, template) { - if (value === "Paid") { - return 'Paid'; - } else if (value === "Comp") { - return 'Comp'; - } else { - return '' + value + ''; - } - }); - - $.addTemplateFormatter("PrintedBadgeFormatter", - function(value, template) { - if (value) { - return ''; - } - } - ); - - $.addTemplateFormatter("PrintedBadgeNumberFormatter", - function(value, template) { - if (value) { - return '' + value + ''; - } - } - ); - - $.addTemplateFormatter("BadgeChangeFormatter", - function(value, template) { - if (value) { - return '' + value.name + ''; - } - } - ); - - - /* This should probably be a checkbox instead, so that we can still send to a terminal's associated printer - if (navigator.userAgent.match(/iPad|iPhone/)) { - $("#pos").append(''); - } - if (navigator.userAgent.match(/Android/)) { - $("#pos").append(''); - } - */ - - $.addTemplateFormatter("HoldTypeFormatter", - function(value, template) { - if (value === null) { - return ''; - } else { - return '' + value + ''; - } - }); - - $.ajaxSetup({ - beforeSend: function(xhr, settings) { - if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) { - // Only send the token to relative URLs i.e. locally. - xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken')); - } - } - }); - - const fadeout_cart = function(callback) { - $("#cart").fadeOut(); - $("#total").fadeOut(400, callback); - } - - const fadein_cart = function(callback) { - $("#cart").fadeIn(); - $("#total").fadeIn(400, callback); - } - - refresh_cart = function(callback) { - $("#cart-error").fadeOut(); - $.getJSON(URL_REGISTRATION_ONSITE_ADMIN_CART, function(data) { - cartData = data; - let enable_print = false; - let onHold = false; - if (data.success) { - cartTemplateData = []; - let orderItemsData = {}; - $.each( data.result, function( key, val ) { - let level = "?"; - let price = "?"; - let state = "danger"; - if (val.effectiveLevel != null) { - level = val.effectiveLevel.name; - price = val.effectiveLevel.price; - state = ""; - } - - cartTemplateData.push({ - name : val.firstName + ' ' + val.lastName, - badgeName : val.badgeName, - badgeNumber : val.badgeNumber, - age : val.age, - abandoned : val.abandoned, - level : level, - price : price, - printed : val.printed, - delete_id : "delete-" + val.id, - items_id: "order-items-" + val.id, - state : state, - holdType : val.holdType, - discount: val.discount, - level_discount: val.level_discount, - level_subtotal: val.level_subtotal, - level_total: val.level_total - }); - orderItemsData[val.id] = val.attendee_options; - - if (val.discount) { - orderItemsData[val.id].push({ - quantity: 1, - item: "Discount "+val.discount.name, - price: "-"+val.discount.amount_off+" / "+val.discount.percent_off+"%", - total: "-$" + val.level_discount - }); - } - - if (!!val.holdType) { - onHold = true; - enable_print = false; - } - }); - $("#total").html(""); - $("#cart").html(""); - $("#cart").loadTemplate($("#cartTemplate"), cartTemplateData); - $(".remove-badge").click(remove_badge); - $(".link-badge").click(link_badge); - - $.each(orderItemsData, function(key, val) { - $("#order-items-"+key).loadTemplate($("#itemRowTemplate"), val); - }); - - let price = parseFloat(data.total); - if (((!isNaN(price))) && (!onHold)) { - $("#total").loadTemplate($("#totalTemplate"), data); - $("#cash_button").removeAttr("disabled"); - $("#credit_button").removeAttr("disabled"); - } else { - $("#cash_button").attr("disabled", "disabled"); - $("#credit_button").attr("disabled", "disabled"); - } - - if (isNaN(price) || (price === 0)) { - $("#cash_button").attr("disabled", "disabled"); - $("#credit_button").attr("disabled", "disabled"); - } - - get_printable(); - } - - if (typeof(callback) === 'function') { - callback(); - } - - }) - .fail(function (data) { - $("#cart-error").html("A server error occurred while refreshing the cart
"+data.message).fadeIn(); - }); - }; - - refresh_cart(); - $("#refresh_button").click(function (e) { - e.preventDefault(); - fadeout_cart(function () { - refresh_cart(function () { - fadein_cart(); - }); - - }) - }); - - $(".add-badge").click(function (e) { - e.preventDefault(); - let id = $(this).data("id"); - $(this).attr("disabled", "disabled"); - $.getJSON(URL_REGISTRATION_ONSITE_ADMIN_ADD_TO_CART, { id : id }, function (data) { - if (data.success) { - refresh_cart(); - } else { - alert("Error while adding to cart"); - } - }); - }); - - $("#open-terminal").click(function (e) { - e.preventDefault(); - $.getJSON(URL_REGISTRATION_OPEN_TERMINAL, {}, function (data) { - if (!data.success) { - alert("Error while opening terminal: " + data.message); - } - }); - }); - - $("#close-terminal").click(function (e) { - e.preventDefault(); - $.getJSON(URL_REGISTRATION_CLOSE_TERMINAL, {}, function (data) { - if (!data.success) { - alert("Error while closing terminal: " + data.message); - } - }); - }); - - $("#ready-terminal").click(function (e) { - e.preventDefault(); - $.getJSON(URL_REGISTRATION_READY_TERMINAL, {}, function (data) { - if (!data.success) { - alert("Error while sending ready terminal message: " + data.message); - } - }); - }); - - // Keyboard shortcut bindings - document.addEventListener('keydown', function (e) { - if (e.ctrlKey == true) { - if (e.key.toLowerCase() == 'p') { - e.preventDefault(); - $("#print_button").click(); - } - } - - if (e.altKey == true) { - if (e.key.toLowerCase() == 'c') { - e.preventDefault(); - $("#credit_button").click(); - } else if (e.key.toLowerCase() == 'p') { - e.preventDefault(); - $("#print_button").click(); - } else if (e.key.toLowerCase() == 'n') { - e.preventDefault(); - $("#ready-terminal").click(); - } else if (e.key.toLowerCase() == 'o') { - e.preventDefault(); - $("#open-terminal").click(); - } else if (e.key.toLowerCase() == 'l') { - e.preventDefault(); - $("#close-terminal").click() - } else if (e.key.toLowerCase() == 'a') { - e.preventDefault(); - $("#clear-cart").click(); - } else if (e.key.toLowerCase() =='s') { - e.preventDefault(); - $("#clear-scans-log").click(); - } - } - }) - - $("#open-drawer").click(function (e) { - e.preventDefault(); - let raw_amount = prompt("Enter initial amount in drawer"); - if (raw_amount == null || raw_amount === "") { - return - } - let parsed = parseFloat(raw_amount.match(/(\d+).?(\d{0,2})?/)); - if (parsed === 0 || isNaN(parsed)) { - return - } - let data = { - 'amount' : parsed - } - $.post(URL_REGISTRATION_OPEN_DRAWER, data, function (data) { - if (!data.success) { - alert("Error while opening drawer: " + data.message); - } else { - alert("Successfully opened drawer!") - } - },'json'); - }); - - $("#cash-deposit").click(function (e) { - e.preventDefault(); - let raw_amount = prompt("Enter amount added to drawer"); - if (raw_amount == null || raw_amount === "") { - return - } - let parsed = parseFloat(raw_amount.match(/(\d+).?(\d{0,2})?/)); - if (parsed === 0) { - return - } - let data = { - 'amount' : parsed - } - $.post(URL_REGISTRATION_CASH_DEPOSIT, data, function (data) { - if (!data.success) { - alert("Error recording cash deposit: " + data.message); - } else { - alert("Successfully recorded cash deposit!") - } - },'json'); - }); - - $("#safe-drop").click(function (e) { - e.preventDefault(); - $("#no-sale").trigger("click"); - let raw_amount = prompt("Enter amount dropped into safe"); - if (raw_amount == null || raw_amount === "") { - return - } - let parsed = parseFloat(raw_amount.match(/(\d+).?(\d{0,2})?/)); - if (parsed === 0) { - return - } - let data = { - 'amount' : parsed, - } - $.post(URL_REGISTRATION_SAFE_DROP, data, function (data) { - if (!data.success) { - alert("Error while recording safe drop: " + data.message); - } else { - alert("Successfully recorded safe drop!") - } - },'json'); - }); - - $("#cash-pickup").click(function (e) { - e.preventDefault(); - $("#no-sale").trigger("click"); - let raw_amount = prompt("Enter amount picked up from drawer"); - if (raw_amount == null || raw_amount === "") { - return - } - let parsed = parseFloat(raw_amount.match(/(\d+).?(\d{0,2})?/)); - if (parsed === 0) { - return - } - let data = { - 'amount' : parsed - } - $.post(URL_REGISTRATION_CASH_PICKUP, data, function (data) { - if (!data.success) { - alert("Error recording cash pickup: " + data.message); - } else { - alert("Successfully recorded cash pickup!") - } - },'json'); - }); - - $("#close-drawer").click(function (e) { - e.preventDefault(); - $("#no-sale").trigger("click"); - let raw_amount = prompt("Enter final amount in drawer"); - if (raw_amount == null || raw_amount === "") { - return - } - let parsed = parseFloat(raw_amount.match(/(\d+).?(\d{0,2})?/)); - if (parsed === 0) { - return - } - let data = { - 'amount' : parsed - } - $.post(URL_REGISTRATION_CLOSE_DRAWER, data, function (data) { - if (!data.success) { - alert("Error while closing drawer: " + data.message); - } else { - alert("Successfully closed drawer!") - } - },'json'); - }); - - $("#no-sale").click(function (e) { - e.preventDefault(); - $.getJSON(URL_REGISTRATION_NO_SALE, {}, function (data) { - if (!data.success) { - alert("Error while opening drawer: " + data.message); - } - }); - }); - - - $("#credit_button").click(function (e) { - e.preventDefault(); - $.getJSON(URL_REGISTRATION_ENABLE_PAYMENT, {}, function (data) { - if (!data.success) { - alert("Error while closing terminal: " + data.message); - } - }); - }); - - $("#cash_button").click(function (e) { - e.preventDefault(); - let tendered = prompt("Enter tendered amount"); - let parsed = parseFloat(tendered.match(/(\d+).?(\d{0,2})?/)); - let total = parseFloat(cartData.total); - if (parsed < total) { - alert("Insufficient payment. (Split tender unsupported)"); - return; - } - - let change = parsed - total; - - let data = { - 'reference' : cartData.reference, - 'total' : total, - 'tendered' : parsed - } - $.getJSON(URL_REGISTRATION_COMPLETE_CASH_TRANSACTION, data, function (data) { - if (data.success) { - refresh_cart(); - } else { - alert("Error while posting transaction to server"); - } - }); - - alert("Change: $" + change); - - }); - - - let remove_badge = function (e) { - e.preventDefault(); - let id = $(this).attr("id").split("-")[1]; - $.getJSON(URL_REGISTRATION_ONSITE_REMOVE_FROM_CART, { id : id }, function (data) { - if (data.success) { - refresh_cart(); - } else { - alert("Error while removing from cart"); - } - }).fail(function() { - window.reload(); - }); - }; - - let link_badge = function (e) { - e.preventDefault(); - let id = $(this).attr("id").split("-")[1]; - let url = URL_ADMIN_REGISTRATION_BADGE.replace('0', id); - window.open(url, '_blank'); - }; - - let add_discount = function(e) { - } - - $("#pos").change(function () { - $("#terminal_form").submit(); - }); - - $("#print_button").click(print_badges); - - - -}); diff --git a/registration/static/js/onsite/onsite_push.js b/registration/static/js/onsite/onsite_push.js deleted file mode 100644 index 7adb5b33..00000000 --- a/registration/static/js/onsite/onsite_push.js +++ /dev/null @@ -1,194 +0,0 @@ - -const shc_source = $("#shc-template").html(); -const shc_template = Handlebars.compile(shc_source); - -const url_source = $("#url-template").html() -const url_template = Handlebars.compile(url_source); - -function get_topic(topic) { - return MQTT_BASE_TOPIC + "/" + topic; -} - -function send_notification(message) { - if (!("Notification" in window)) { - alert(message); - } else if (Notification.permission === "granted") { - var notification = new Notification(message); - } -} - -function isExpired(datetime) { - return (new Date().getTime() > datetime.getTime()); -} - -function bind_close_panel() { - $(".close-panel").click(function (evt) { - $(this).parent().parent().parent().remove(); - localStorage.removeItem($(this).data("item")); - }); -} - -function load_id_scan() { - let parsed = JSON.parse(localStorage.getItem("id_scan")); - if (parsed == null) { - return; - } - - parsed.age = getAge(parseDate(parsed.dob)); - parsed.expired = isExpired(parseDate(parsed.expiry)); - - let node = document.createElement("div"); - let source = $("#scan-template").html(); - let template = Handlebars.compile(source); - - node.innerHTML = template(parsed); - $("#scan-log").append(node); - - bind_close_panel(); -} - -function get_age_days(datetime) { - let diff = new Date().getTime() - datetime.getTime(); - return Math.floor(diff / (1000 * 60 * 60 * 24)); -} - -function load_shc_scan() { - let parsed = JSON.parse(localStorage.getItem("shc_scan")) - if (parsed == null) { - return; - } - - // verified, and not trusted: warning - parsed.verification.class = "warning"; - parsed.verification.status = "Partially Verified"; - // Trusted and verified: success - if (parsed.verification.trusted) { - parsed.verification.class = "success"; - parsed.verification.status = "Verified"; - } - // not verified: danger - if (!parsed.verification.verified) { - parsed.verification.class = "danger"; - parsed.verification.status = "Not Verified"; - } - - parsed.vaccines.forEach(function (item, idx, arr) { - item.age = get_age_days(parseDate(item.date)); - item.cls = (item.age < 14 ? "danger" : ""); - }); - - // Check if birthdate matches an ID scan if it's there - let id_scan = JSON.parse(localStorage.getItem("id_scan")); - if (id_scan != null) { - parsed.dob_matches = id_scan.dob == parsed.birthday; - parsed.name_matches = (id_scan.first + " " + id_scan.last).toUpperCase() == parsed.name.toUpperCase(); - } else { - parsed.dob_matches = true; - parsed.name_matches = true; - } - - let node = document.createElement("div"); - node.innerHTML = shc_template(parsed); - $("#scan-log").append(node); - bind_close_panel(); -} - -function load_url_scan() { - let parsed = JSON.parse(localStorage.getItem("url_scan")) - if (parsed == null) { - return; - } - - let node = document.createElement("div"); - node.innerHTML = url_template(parsed); - $("#scan-log").append(node); - bind_close_panel(); -} - -function clear_scan_log() { - localStorage.removeItem("id_scan"); - localStorage.removeItem("shc_scan"); - localStorage.removeItem("url_scan"); - $("#scan-log").html(""); -} - -$(document).ready(function () { - load_id_scan(); - load_shc_scan(); - load_url_scan(); - - $("#clear-scans-log").click(clear_scan_log); -}) - -if (MQTT_ENABLED) { - try { - let notification_promise = Notification.requestPermission(); - } catch { - console.log("Notification permission request failed"); - } - - const client = mqtt.connect(MQTT_BROKER, MQTT_OPTIONS); - - console.log(client); - - client.on('connect', function () { - let topic = get_topic("#"); - console.log("MQTT subscribe to", topic); - client.subscribe(topic, function(err) { - if (!err) { - client.publish(get_topic("admin_presence"), '"Hello! :3"'); - } - }); - }) - - client.on('error', function (error) { - console.log("MQTT error: ", error); - $("#client-error").fadeIn(); - }); - - client.on('reconnect', function(error) { - console.log("MQTT reconnecting:", error); - $("#client-error").fadeOut(); - }); - - client.on('message', function(topic, message) { - console.log("MQTT message:", topic, message.toString()); - - let payload = null; - try { - payload = JSON.parse(message.toString()); - } catch (SyntaxError) { - } - - if (topic === get_topic("refresh")) { - refresh_cart(); - } - - if (topic == get_topic("open")) { - window.open(payload.url); - localStorage.setItem("url_scan", message.toString()); - load_url_scan(); - } - - if (topic == get_topic("notification")) { - send_notification(payload.text); - } - - if (topic == get_topic("alert")) { - alert(payload.text); - } - - if (topic == get_topic("scan/id")) { - // Cache the scan in local storage, so it survives page refreshes - localStorage.setItem('id_scan', message.toString()); - $("input[name=search]").val(payload.last); - $("#search_form").submit(); - } - - if (topic == get_topic("scan/shc")) { - // SmartHealthCard vaccine QR code scanned - localStorage.setItem('shc_scan', message.toString()); - load_shc_scan(); - } - }); -} \ No newline at end of file From 96cb4a9d76f084d0973c997ac07169da2dda8842 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Fri, 8 Nov 2024 00:28:27 -0500 Subject: [PATCH 06/44] Significantly improve search result quality. --- .../frontend/src/admin/attendee-search.tsx | 9 ++-- .../frontend/src/admin/scan-actions.tsx | 6 +-- registration/views/onsite_admin.py | 51 ++++++++++++------- 3 files changed, 40 insertions(+), 26 deletions(-) diff --git a/registration/frontend/src/admin/attendee-search.tsx b/registration/frontend/src/admin/attendee-search.tsx index 9dff70ca..3f35e681 100644 --- a/registration/frontend/src/admin/attendee-search.tsx +++ b/registration/frontend/src/admin/attendee-search.tsx @@ -145,16 +145,13 @@ export const AttendeeSearch: Component<{ }> = (props) => { const config = useContext(ConfigContext); - const [results, { refetch }] = createResource( - props.searchQuery, - async (query) => getSearchResults(config.urls, query) + const [results, { refetch }] = createResource(props.searchQuery, (query) => + getSearchResults(config.urls, query) ); let searchInputRef: HTMLInputElement; createEffect(() => { - console.log(`Updating search query: ${props.searchQuery()}`); - if (props.searchQuery()) { searchInputRef.value = props.searchQuery(); } @@ -170,7 +167,7 @@ export const AttendeeSearch: Component<{
diff --git a/registration/frontend/src/admin/scan-actions.tsx b/registration/frontend/src/admin/scan-actions.tsx index f8228e07..f30a35f4 100644 --- a/registration/frontend/src/admin/scan-actions.tsx +++ b/registration/frontend/src/admin/scan-actions.tsx @@ -80,9 +80,9 @@ export const ScanPanel: Component<{ }); createEffect(() => { - const name = scanLog().id?.last; - if (name) { - props.gotScannedName(name); + const id = scanLog()?.id; + if (id) { + props.gotScannedName(`${id.first} ${id.last}`); } }); diff --git a/registration/views/onsite_admin.py b/registration/views/onsite_admin.py index a15ce6b1..9d0f19d4 100644 --- a/registration/views/onsite_admin.py +++ b/registration/views/onsite_admin.py @@ -10,13 +10,14 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import permission_required from django.contrib.messages import get_messages -from django.db.models import Q, Sum +from django.db.models import Q, Sum, F, Value, Func from django.http import JsonResponse from django.shortcuts import redirect, render from django.template.loader import render_to_string from django.urls import reverse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt +from django.contrib.postgres.search import TrigramSimilarity from registration import admin, mqtt, payments, printing from registration.admin import TWOPLACES @@ -170,27 +171,43 @@ def onsite_admin_search(request): if query is None: return redirect("registration:onsite_admin") - results = Badge.objects.filter( - Q(attendee__lastName__icontains=query) - | Q(attendee__preferredName__icontains=query) - | Q(attendee__firstName__icontains=query), - Q(event=event), - ).prefetch_related("attendee", "event") - data = [] - for result in results: - data.append({ - "id": result.id, - "edit_url": reverse("admin:registration_badge_change", args=(result.id,)), + + def collectBadges(badges): + for badge in badges: + data.append({ + "id": badge.id, + "edit_url": reverse("admin:registration_badge_change", args=(badge.id,)), "attendee": { - "firstName": result.attendee.firstName, - "lastName": result.attendee.lastName, - "preferredName": result.attendee.preferredName, + "firstName": badge.attendee.firstName, + "lastName": badge.attendee.lastName, + "preferredName": badge.attendee.preferredName, }, - "badgeName": result.badgeName, - "abandoned": result.abandoned, + "badgeName": badge.badgeName, + "abandoned": badge.abandoned, }) + query = query.strip() + + try: + badge_id = int(query) + badges = Badge.objects.filter(event=event, badgeNumber=badge_id) + collectBadges(badges) + except: + pass + + fullName = Func(F("attendee__firstName"), Value(" "), F("attendee__lastName"), function="CONCAT") + greaterSimilarity = Func("name_similarity", "badge_similarity", function="GREATEST") + + results = Badge.objects.annotate( + name_similarity=TrigramSimilarity(fullName, query), + badge_similarity=TrigramSimilarity("badgeName", query), + ).filter( + Q(event=event) & (Q(name_similarity__gte=0.1) | Q(badge_similarity__gte=0.1)) + ).order_by(greaterSimilarity).reverse().prefetch_related("attendee")[:100] + + collectBadges(results) + return JsonResponse({"success": True, "results": data}) From d5b60228da2d23e88cbb0b8d4fe57406e35dfbeb Mon Sep 17 00:00:00 2001 From: Syfaro Date: Fri, 8 Nov 2024 12:46:05 -0500 Subject: [PATCH 07/44] Remove all global state. --- .../frontend/src/admin/attendee-search.tsx | 262 ------------ .../components/AttendeeSearch.tsx | 143 +++++++ .../components/BadgeTableLoader.tsx | 46 +++ .../components/BadgeTableRow.tsx | 64 +++ .../src/admin/attendee-search/index.ts | 42 ++ .../frontend/src/admin/cart/cart-manager.ts | 79 +++- .../admin/cart/components/ActionButton.tsx | 47 +++ .../src/admin/cart/components/Cart.tsx | 7 +- .../src/admin/cart/components/CartActions.tsx | 226 ++++------ .../src/admin/cart/components/CartEntries.tsx | 44 +- registration/frontend/src/admin/cart/index.ts | 4 + registration/frontend/src/admin/mqtt.ts | 188 +++++---- registration/frontend/src/admin/onsite.tsx | 33 +- .../frontend/src/admin/scan-actions.tsx | 386 ------------------ .../src/admin/scan/components/CloseButton.tsx | 7 + .../src/admin/scan/components/IdEntry.tsx | 72 ++++ .../admin/scan/components/MismatchedData.tsx | 18 + .../src/admin/scan/components/ScanPanel.tsx | 123 ++++++ .../src/admin/scan/components/ScanPii.tsx | 41 ++ .../src/admin/scan/components/ShcEntry.tsx | 73 ++++ .../src/admin/scan/components/UrlEntry.tsx | 20 + registration/frontend/src/admin/scan/index.ts | 57 +++ .../frontend/src/entrypoints/admin.ts | 6 - 23 files changed, 1037 insertions(+), 951 deletions(-) delete mode 100644 registration/frontend/src/admin/attendee-search.tsx create mode 100644 registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx create mode 100644 registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx create mode 100644 registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx create mode 100644 registration/frontend/src/admin/attendee-search/index.ts create mode 100644 registration/frontend/src/admin/cart/components/ActionButton.tsx create mode 100644 registration/frontend/src/admin/cart/index.ts delete mode 100644 registration/frontend/src/admin/scan-actions.tsx create mode 100644 registration/frontend/src/admin/scan/components/CloseButton.tsx create mode 100644 registration/frontend/src/admin/scan/components/IdEntry.tsx create mode 100644 registration/frontend/src/admin/scan/components/MismatchedData.tsx create mode 100644 registration/frontend/src/admin/scan/components/ScanPanel.tsx create mode 100644 registration/frontend/src/admin/scan/components/ScanPii.tsx create mode 100644 registration/frontend/src/admin/scan/components/ShcEntry.tsx create mode 100644 registration/frontend/src/admin/scan/components/UrlEntry.tsx create mode 100644 registration/frontend/src/admin/scan/index.ts diff --git a/registration/frontend/src/admin/attendee-search.tsx b/registration/frontend/src/admin/attendee-search.tsx deleted file mode 100644 index 3f35e681..00000000 --- a/registration/frontend/src/admin/attendee-search.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { - Accessor, - Component, - createEffect, - createResource, - createSignal, - For, - Setter, - Show, - useContext, -} from "solid-js"; - -import { ConfigContext } from "./providers/config-provider"; -import { ApisUrls, CSRF_TOKEN } from "../entrypoints/admin"; -import { CartManager } from "./cart/cart-manager"; - -interface BadgeResult { - id: number; - edit_url: string; - attendee: Attendee; - badgeName: string; - abandoned: string; -} - -interface Attendee { - firstName: string; - lastName: string; - preferredName?: string; -} - -async function getSearchResults( - urls: ApisUrls, - query: string -): Promise { - // Clear results if we search for an empty string. - if (query.trim().length === 0) { - return []; - } - - let formData = new FormData(); - formData.set("search", query); - - const resp = await fetch(urls.onsite_admin_search, { - method: "POST", - body: formData, - headers: { - "x-csrftoken": CSRF_TOKEN, - }, - }); - const data = await resp.json(); - - return data["results"]; -} - -const BadgeTableRow: Component<{ - cartManager: CartManager; - badge: BadgeResult; -}> = (props) => { - return ( - - -
- {`${props.badge.attendee.firstName} ${props.badge.attendee.lastName}`} -
- - -
- Preferred Name: - {props.badge.attendee.preferredName} -
-
- - {props.badge.badgeName} - {props.badge.abandoned} - -
- - - - - - - -
- - - ); -}; - -const BadgeTableLoader: Component = () => { - return ( - - -
-
Full Name
-
- - -
-
Badge Name
-
- - -
-
Status
-
- - -
- - - - - - - -
- - - ); -}; - -export const AttendeeSearch: Component<{ - cartManager: CartManager; - searchQuery: Accessor; - setSearchQuery: Setter; -}> = (props) => { - const config = useContext(ConfigContext); - - const [results, { refetch }] = createResource(props.searchQuery, (query) => - getSearchResults(config.urls, query) - ); - - let searchInputRef: HTMLInputElement; - - createEffect(() => { - if (props.searchQuery()) { - searchInputRef.value = props.searchQuery(); - } - }); - - return ( -
-
-
-
-
-
Attendee Search
- - -
-
- -
-
{ - ev.preventDefault(); - props.setSearchQuery(searchInputRef.value); - }} - > -
-

- { - if (ev.target.value.length === 0) { - props.setSearchQuery(""); - } - }} - /> -

- -

- -

-
-
-
- -
- - - - - - - - - - - }> - 0} - fallback={ - - - - } - > - - {(badge, index) => ( - - )} - - - - -
Legal NameBadge NameStatus
No results.
-
-
-
-
- ); -}; diff --git a/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx b/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx new file mode 100644 index 00000000..8c78b88d --- /dev/null +++ b/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx @@ -0,0 +1,143 @@ +import { + Accessor, + Component, + createEffect, + createResource, + For, + Setter, + Show, + useContext, +} from "solid-js"; + +import { ConfigContext } from "../../providers/config-provider"; +import { CartManager } from "../../cart"; +import { getSearchResults } from ".."; +import { BadgeTableRow } from "./BadgeTableRow"; +import { BadgeTableLoader } from "./BadgeTableLoader"; + +export const AttendeeSearch: Component<{ + cartManager: CartManager; + searchQuery: Accessor; + setSearchQuery: Setter; +}> = (props) => { + const config = useContext(ConfigContext); + + const [results, { refetch }] = createResource(props.searchQuery, (query) => + getSearchResults(config.urls, query) + ); + + let searchInputRef: HTMLInputElement; + + createEffect(() => { + const query = props.searchQuery(); + + if (query) { + searchInputRef.value = query; + } + }); + + const noResults = ( + + No results. + + ); + + return ( +
+
+
+
+
+
Attendee Search
+ + +
+
+ +
+
{ + ev.preventDefault(); + props.setSearchQuery(searchInputRef.value); + }} + > +
+

+ { + if (ev.target.value.length === 0) { + props.setSearchQuery(""); + } + }} + /> +

+ +

+ +

+
+
+
+ +
+ + + + + + + + + + + } + > + 0} fallback={noResults}> + + {(badge, index) => ( + + )} + + + + +
Legal NameBadge NameStatus
+
+
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx b/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx new file mode 100644 index 00000000..15f8a6da --- /dev/null +++ b/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx @@ -0,0 +1,46 @@ +import { Component, For } from "solid-js"; + +export const BadgeTableLoader: Component<{ count?: number }> = (props) => { + return ( + + {(_, index) => } + + ); +}; + +const Row: Component = () => { + return ( + + +
+
Full Name
+
+ + +
+
Badge Name
+
+ + +
+
Status
+
+ + +
+ + + + + + + +
+ + + ); +}; diff --git a/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx b/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx new file mode 100644 index 00000000..415da85e --- /dev/null +++ b/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx @@ -0,0 +1,64 @@ +import { Component, Show } from "solid-js"; + +import { CartManager } from "../../cart"; +import { BadgeResult } from ".."; + +export const BadgeTableRow: Component<{ + cartManager: CartManager; + badge: BadgeResult; +}> = (props) => { + const hasPreferredName = () => + props.badge.attendee.preferredName && + props.badge.attendee.preferredName.localeCompare( + props.badge.attendee.firstName + ) !== 0; + + const alreadyInCart = () => props.cartManager.alreadyInCart(props.badge.id); + + return ( + + +
+ {`${props.badge.attendee.firstName} ${props.badge.attendee.lastName}`} +
+ + +
+ Preferred: + + {props.badge.attendee.preferredName} + +
+
+ + {props.badge.badgeName} + {props.badge.abandoned} + +
+ + + + + + + +
+ + + ); +}; diff --git a/registration/frontend/src/admin/attendee-search/index.ts b/registration/frontend/src/admin/attendee-search/index.ts new file mode 100644 index 00000000..5801c198 --- /dev/null +++ b/registration/frontend/src/admin/attendee-search/index.ts @@ -0,0 +1,42 @@ +import { ApisUrls, CSRF_TOKEN } from "../../entrypoints/admin"; +import { AttendeeSearch } from "./components/AttendeeSearch"; + +export { AttendeeSearch }; + +export interface BadgeResult { + id: number; + edit_url: string; + attendee: Attendee; + badgeName: string; + abandoned: string; +} + +export interface Attendee { + firstName: string; + lastName: string; + preferredName?: string; +} + +export async function getSearchResults( + urls: ApisUrls, + query: string +): Promise { + // Clear results if we search for an empty string. + if (query.trim().length === 0) { + return []; + } + + let formData = new FormData(); + formData.set("search", query); + + const resp = await fetch(urls.onsite_admin_search, { + method: "POST", + body: formData, + headers: { + "x-csrftoken": CSRF_TOKEN, + }, + }); + const data = await resp.json(); + + return data["results"]; +} diff --git a/registration/frontend/src/admin/cart/cart-manager.ts b/registration/frontend/src/admin/cart/cart-manager.ts index 24651e37..9f0409bd 100644 --- a/registration/frontend/src/admin/cart/cart-manager.ts +++ b/registration/frontend/src/admin/cart/cart-manager.ts @@ -1,24 +1,32 @@ -import { Setter } from "solid-js"; +import { Accessor, createSignal, Setter } from "solid-js"; import { ApisUrls, CSRF_TOKEN } from "../../entrypoints/admin"; import emitter from "../mqtt"; +import MqttClient from "../mqtt"; export class CartManager { - urls: ApisUrls; - setCartEntries: Setter; + private urls: ApisUrls; + private mqtt: MqttClient; - constructor(urls: ApisUrls, setCartEntries: Setter) { + public cartEntries: Accessor; + private setCartEntries: Setter; + + constructor(urls: ApisUrls, mqtt: MqttClient) { this.urls = urls; + + const [cartEntries, setCartEntries] = createSignal(); + this.cartEntries = cartEntries; this.setCartEntries = setCartEntries; - emitter.on("refresh", this.refreshCart.bind(this)); + this.mqtt = mqtt; + mqtt.emitter.on("refresh", this.refreshCart.bind(this)); } close() { - emitter.off("refresh", this.refreshCart.bind(this)); + this.mqtt.emitter.off("refresh", this.refreshCart.bind(this)); } - private updateCart(data: CartResponse) { + private updateCart(data: FallibleRequest) { if (!data.success) { alert("Failed to update cart"); window.location.reload(); @@ -90,7 +98,7 @@ export class CartManager { reference: string, total: string, tendered: string - ): Promise { + ): Promise> { let url = new URL( this.urls.complete_cash_transaction, window.location.href @@ -108,7 +116,7 @@ export class CartManager { return data; } - public async enableCardPayment(): Promise { + public async enableCardPayment(): Promise> { const resp = await fetch(this.urls.enable_payment, { headers: { "x-csrftoken": CSRF_TOKEN, @@ -120,8 +128,9 @@ export class CartManager { } public async printBadges( - ids: number[] - ): Promise { + ids: number[], + mqttPrint: boolean = false + ): Promise> { const assignResp = await fetch(this.urls.assign_badge_number, { method: "POST", headers: { @@ -135,10 +144,10 @@ export class CartManager { }) ), }); - const assignData: FallibleRequest = await assignResp.json(); + const assignData: FallibleRequest = await assignResp.json(); if (!assignData.success) { - return assignData; + return { success: false }; } let url = new URL(this.urls.onsite_print_badges, window.location.href); @@ -151,7 +160,25 @@ export class CartManager { "x-csrftoken": CSRF_TOKEN, }, }); - const printData: BadgePrintResponse = await printResp.json(); + const printData: FallibleRequest = + await printResp.json(); + + if (printData.success && mqttPrint) { + console.debug(`Wants MQTT print for ${printData.file}`); + + let url = new URL(this.urls.pdf, window.location.href); + url.searchParams.append("file", printData.file); + + this.mqtt.publishMessage( + "action", + JSON.stringify({ + action: "print", + url, + }) + ); + } else { + console.debug("Not using MQTT print"); + } await this.clearCart(); await this.refreshCart(); @@ -167,13 +194,21 @@ export class CartManager { url.pathname = url.pathname.replace("0", id.toString()); return url.toString(); } -} -export interface FallibleRequest { - success: boolean; + public alreadyInCart(id: number): boolean { + return ( + this.cartEntries()?.result?.some((badge) => badge.id === id) || false + ); + } } -export interface CartResponse extends FallibleRequest { +export type FallibleRequest = + | { + success: false; + } + | ({ success: true } & T); + +export interface CartResponse { charityDonation: string; order_id: number; orgDonation: string; @@ -189,13 +224,13 @@ export interface Badge { abandoned: string; age: number; badgeName: string; - badgeNumber: number | null; + badgeNumber?: number; firstName: string; lastName: string; - holdType: string | null; + holdType?: string; printed: boolean; effectiveLevel: EffectiveLevel; - discount: Discount | null; + discount?: Discount; level_subtotal: string; level_discount: string; level_total: string; @@ -220,7 +255,7 @@ export interface AttendeeOption { total: string; } -export interface BadgePrintResponse extends FallibleRequest { +export interface BadgePrintResponse { file: string; next: string; url: string; diff --git a/registration/frontend/src/admin/cart/components/ActionButton.tsx b/registration/frontend/src/admin/cart/components/ActionButton.tsx new file mode 100644 index 00000000..0f59b5ea --- /dev/null +++ b/registration/frontend/src/admin/cart/components/ActionButton.tsx @@ -0,0 +1,47 @@ +import { Component, JSX, Setter } from "solid-js"; + +export type ActionButtonKey = "cash" | "card" | "print"; + +export type ActionButtonProps = { + button: ActionButtonKey; + class: string; + disabled: boolean; + loadingButton?: ActionButtonKey; + setLoadingButton: Setter; + action: (ev: MouseEvent) => Promise; + children: JSX.Element; +}; + +export const ActionButton: Component = (props) => { + let classes = `button is-fullwidth ${props.class}`; + + return ( +
+ +
+ ); +}; + +async function trackLoadingButton( + setLoadingButton: Setter, + button: ActionButtonKey, + action: Promise +): Promise { + setLoadingButton(button); + const resp = await action; + setLoadingButton(null); + return resp; +} diff --git a/registration/frontend/src/admin/cart/components/Cart.tsx b/registration/frontend/src/admin/cart/components/Cart.tsx index 5e3566be..621a8eb6 100644 --- a/registration/frontend/src/admin/cart/components/Cart.tsx +++ b/registration/frontend/src/admin/cart/components/Cart.tsx @@ -7,7 +7,6 @@ import { CartActions } from "./CartActions"; export const Cart: Component<{ cartManager: CartManager; - cartEntries: CartResponse; }> = (props) => { const [refresh, { refetch: refreshCart }] = createResource(async () => { await props.cartManager.refreshCart(); @@ -61,12 +60,12 @@ export const Cart: Component<{
- - + +
); diff --git a/registration/frontend/src/admin/cart/components/CartActions.tsx b/registration/frontend/src/admin/cart/components/CartActions.tsx index f83110da..f33bff1f 100644 --- a/registration/frontend/src/admin/cart/components/CartActions.tsx +++ b/registration/frontend/src/admin/cart/components/CartActions.tsx @@ -1,112 +1,19 @@ -import { - Component, - createEffect, - createMemo, - createSignal, - JSX, - Setter, - useContext, -} from "solid-js"; +import { Component, createMemo, createSignal, useContext } from "solid-js"; import { Big } from "big.js"; -import { BadgePrintResponse, CartManager, CartResponse } from "../cart-manager"; +import { CartManager, CartResponse } from "../cart-manager"; import { ConfigContext } from "../../providers/config-provider"; -import { publishMessage } from "../../mqtt"; +import { ActionButton, ActionButtonKey } from "./ActionButton"; const PRINTABLE_STATUS = new Set(["Paid", "Comp", "Staff", "Dealer"]); -type ActionButton = "cash" | "card" | "print"; - -async function trackLoadingButton( - setLoadingButton: Setter, - button: ActionButton, - action: Promise -): Promise { - setLoadingButton(button); - const resp = await action; - setLoadingButton(null); - return resp; -} - -async function attemptCashPayment( - manager: CartManager, - reference: string, - total: string -) { - const totalAmount = new Big(total); - - const tendered = prompt("Enter tendered amount"); - let tenderedAmount: Big; - try { - tenderedAmount = new Big(tendered); - } catch (err) { - alert("Invalid amount."); - return; - } - - if (tenderedAmount.lt(totalAmount)) { - alert("Insufficient payment, split tender unsupported."); - return; - } - - let change = tenderedAmount.sub(totalAmount); - - const resp = await manager.applyCashPayment(reference, total, tendered); - if (resp.success) { - manager.refreshCart(); - } else { - alert("Error posting cash transaction."); - return; - } - - alert(`Change: ${change}`); -} - -async function enableCardPayment(manager: CartManager) { - const resp = await manager.enableCardPayment(); - if (!resp.success) { - alert("Error enabling card payment."); - } -} - -async function printBadges( - manager: CartManager, - ids: number[], - mqttPrint: boolean = false, - pdfUrl: string | null = null -) { - const resp = await manager.printBadges(ids); - if (!resp.success) { - alert("Error printing badges."); - return; - } - - // If it was successful, it should always have the correct data. - const data = resp as BadgePrintResponse; - - if (mqttPrint && pdfUrl && publishMessage) { - let url = new URL(pdfUrl, window.location.href); - url.searchParams.append("file", data.file); - - publishMessage( - "action", - JSON.stringify({ - action: "print", - url, - }) - ); - } else { - window.open(data.url, "badge"); - } -} - export const CartActions: Component<{ manager: CartManager; cartEntries: CartResponse; }> = (props) => { const config = useContext(ConfigContext); - const [loadingButton, setLoadingButton] = createSignal(); + const [loadingButton, setLoadingButton] = createSignal(); const hasHold = createMemo( () => @@ -144,7 +51,7 @@ export const CartActions: Component<{ return (
- - - - - - Tender Cash + + - - Tender Cash + + enableCardPayment(props.manager)} > - - - - - Credit/Debit Card + + - + Credit/Debit Card +
- - - - - - Print Badges + + - + Print Badges +
); }; -const LoadableButton: Component<{ - button: ActionButton; - class: string; - disabled: boolean; - loadingButton: ActionButton | null; - setLoadingButton: Setter; - action: (ev: MouseEvent) => Promise; - children: JSX.Element; -}> = (props) => { - let classes = `button is-fullwidth ${props.class}`; +async function attemptCashPayment( + manager: CartManager, + reference: string, + total: string +) { + const totalAmount = new Big(total); - return ( -
- -
- ); -}; + const tendered = prompt("Enter tendered amount"); + let tenderedAmount: Big; + try { + tenderedAmount = new Big(tendered); + } catch (err) { + alert("Invalid amount."); + return; + } + + if (tenderedAmount.lt(totalAmount)) { + alert("Insufficient payment, split tender unsupported."); + return; + } + + let change = tenderedAmount.sub(totalAmount); + + const resp = await manager.applyCashPayment(reference, total, tendered); + if (resp.success) { + manager.refreshCart(); + } else { + alert("Error posting cash transaction."); + return; + } + + alert(`Change: ${change}`); +} + +async function enableCardPayment(manager: CartManager) { + const resp = await manager.enableCardPayment(); + if (!resp.success) { + alert("Error enabling card payment."); + } +} + +async function printBadges( + manager: CartManager, + ids: number[], + mqttPrint: boolean = false +) { + const resp = await manager.printBadges(ids, mqttPrint); + if (!resp.success) { + alert("Error printing badges."); + return; + } + + console.debug(`Got response from badge print: mqttPrint=${mqttPrint}, resp=${resp}`); + + if (!mqttPrint) { + window.open(resp.url, "badge"); + } +} diff --git a/registration/frontend/src/admin/cart/components/CartEntries.tsx b/registration/frontend/src/admin/cart/components/CartEntries.tsx index c3dc579e..d512a929 100644 --- a/registration/frontend/src/admin/cart/components/CartEntries.tsx +++ b/registration/frontend/src/admin/cart/components/CartEntries.tsx @@ -4,20 +4,6 @@ import { Big } from "big.js"; import { CartManager, CartResponse } from "../cart-manager"; import { CartBadge } from "./CartBadge"; -export function cleanMoneyAmount(input: string): string { - if (input == "?") return "0.00"; - - let parsed: Big; - try { - parsed = new Big(input); - } catch (err) { - console.error(`Could not parse money: ${err}`); - return input; - } - - return `$${parsed.toFixed(2)}`; -} - export const CartEntries: Component<{ manager: CartManager; cart: CartResponse; @@ -30,7 +16,9 @@ export const CartEntries: Component<{ options.push({ quantity: 1, item: `Discount ${result.discount.name}`, - price: `-${cleanMoneyAmount(result.discount.amount_off)} / ${result.discount.percent_off}%`, + price: `-${cleanMoneyAmount(result.discount.amount_off)} / ${ + result.discount.percent_off + }%`, total: `-${cleanMoneyAmount(result.level_discount)}`, }); } @@ -46,7 +34,9 @@ export const CartEntries: Component<{ Subtotal: - {cleanMoneyAmount(props.cart.subtotal)} + + {cleanMoneyAmount(props.cart.subtotal)} + Discounts: @@ -60,8 +50,8 @@ export const CartEntries: Component<{ Donation to Convention: {cleanMoneyAmount(props.cart.orgDonation)} - - Total: + + Total: {cleanMoneyAmount(props.cart.total)} @@ -109,3 +99,21 @@ export const CartEntries: Component<{ ); }; + +export function cleanMoneyAmount(input?: string): string { + if (!input || input == "?") return "$0.00"; + + if (input.startsWith("$")) { + input = input.substring(1); + } + + let parsed: Big; + try { + parsed = new Big(input); + } catch (err) { + console.error(`Could not parse money: ${err}`); + return input; + } + + return `$${parsed.toFixed(2)}`; +} diff --git a/registration/frontend/src/admin/cart/index.ts b/registration/frontend/src/admin/cart/index.ts new file mode 100644 index 00000000..76eaaf64 --- /dev/null +++ b/registration/frontend/src/admin/cart/index.ts @@ -0,0 +1,4 @@ +import { Cart } from "./components/Cart"; +import { CartManager, CartResponse } from "./cart-manager"; + +export { Cart, CartManager, CartResponse }; diff --git a/registration/frontend/src/admin/mqtt.ts b/registration/frontend/src/admin/mqtt.ts index 163202db..3c236c0e 100644 --- a/registration/frontend/src/admin/mqtt.ts +++ b/registration/frontend/src/admin/mqtt.ts @@ -2,8 +2,9 @@ import mqtt from "mqtt"; import mitt, { Emitter } from "mitt"; import { ApisMqttConfig } from "../entrypoints/admin"; +import { Accessor, createSignal, Setter } from "solid-js"; -type MqttTopic = +export type MqttTopic = | "refresh" | "open" | "notification" @@ -11,99 +12,114 @@ type MqttTopic = | "scan/id" | "scan/shc"; -const emitter: Emitter> = mitt(); -window["mqttEmitter"] = emitter; +export type MqttEmitter = Emitter>; -const randomClientId = Math.random().toString(16).substr(2, 8); +export default class MqttClient { + public errorMessage: Accessor; + private setErrorMessage: Setter; -function sendNotification(message: string) { - if (Notification.permission === "granted") { - return new Notification(message); - } else { - alert(message); - } -} + public emitter: Emitter>; -export let publishMessage: (topic: string, message: string) => void | null = - null; + private client: mqtt.MqttClient; + private config: ApisMqttConfig; -export function connectToMqtt(config: ApisMqttConfig) { - const mqttErrorMessage = document.getElementById("mqtt-client-error"); + constructor(config: ApisMqttConfig) { + this.config = config; - function getTopic(topic: string): string { - return `${config.auth.base_topic}/${topic}`; - } + const [errorMessage, setErrorMessage] = createSignal(); + this.errorMessage = errorMessage; + this.setErrorMessage = setErrorMessage; + + this.emitter = mitt(); + + const WILDCARD_TOPIC = this.getPrefixedTopic("#"); + + const randomClientId = Math.random().toString(16).substr(2, 8); + this.client = mqtt.connect(config.broker, { + username: config.auth.user, + password: config.auth.token, + clientId: `${config.auth.user}-${randomClientId}`, + clean: true, + }); + + this.client.on("connect", () => { + this.setErrorMessage(undefined); + console.debug(`Subscribing to ${WILDCARD_TOPIC}`); + this.client.subscribe(WILDCARD_TOPIC, (err) => { + if (err) { + console.error(`MQTT subscription failed: ${err}`); + } else { + this.client.publish( + this.getPrefixedTopic("admin_presence"), + JSON.stringify(":3") + ); + } + }); + }); + + this.client.on("error", (err: Error) => { + console.error(`MQTT error: ${err}`); + this.setErrorMessage(err.toString()); + }); + + this.client.on("reconnect", () => { + console.debug("Reconnecting to MQTT"); + }); - const WILDCARD_TOPIC = getTopic("#"); - - const client = mqtt.connect(config.broker, { - username: config.auth.user, - password: config.auth.token, - clientId: `${config.auth.user}-${randomClientId}`, - clean: true, - }); - - publishMessage = (topic, message) => { - console.debug(`Publishing to topic ${topic} with message: ${message}`); - client.publish(getTopic(topic), message); - }; - - client.on("connect", () => { - mqttErrorMessage?.classList?.add("d-none"); - console.debug(`Subscribing to ${WILDCARD_TOPIC}`); - client.subscribe(WILDCARD_TOPIC, (err) => { - if (err) { - console.error(`MQTT subscription failed: ${err}`); + this.client.on("message", (topic, message) => { + let data = message.toString(); + console.debug("MQTT message", topic, data); + + let strippedTopic: MqttTopic; + if (topic.startsWith(config.auth.base_topic)) { + strippedTopic = topic.slice( + config.auth.base_topic.length + 1 + ) as MqttTopic; } else { - client.publish(getTopic("admin_presence"), JSON.stringify(":3")); + console.warn(`Got topic with unexpected prefix: ${topic}`); + return; + } + + let payload = null; + try { + payload = JSON.parse(data); + } catch (err) {} + + switch (strippedTopic) { + case "notification": + if (payload?.["text"]) { + sendNotification(payload?.["text"]); + } + break; + case "alert": + if (payload?.["text"]) { + alert(payload?.["text"]); + } + break; + default: + this.emitter.emit(strippedTopic, payload); + break; } }); - }); - - client.on("error", (err) => { - console.error(`MQTT error: ${err}`); - mqttErrorMessage?.classList?.remove("d-none"); - }); - - client.on("reconnect", () => { - console.debug("Reconnecting to MQTT"); - }); - - client.on("message", (topic, message) => { - let data = message.toString(); - console.debug("MQTT message", topic, data); - - let strippedTopic: MqttTopic; - if (topic.startsWith(config.auth.base_topic)) { - strippedTopic = topic.slice( - config.auth.base_topic.length + 1 - ) as MqttTopic; - } else { - console.warn(`Got topic with unexpected prefix: ${topic}`); - return; - } - - let payload = null; - try { - payload = JSON.parse(data); - } catch (err) {} - - switch (strippedTopic) { - case "notification": - if (payload?.["text"]) { - sendNotification(payload?.["text"]); - } - break; - case "alert": - if (payload?.["text"]) { - alert(payload?.["text"]); - } - break; - default: - emitter.emit(strippedTopic, payload); - break; - } - }); + } + + public disconnect() { + this.client.end(); + } + + public publishMessage(topic: string, payload: string) { + this.client.publish(this.getPrefixedTopic(topic), payload); + } + + private getPrefixedTopic(topic: string): string { + return `${this.config.auth.base_topic}/${topic}`; + } } -export default emitter; +function sendNotification(message: string) { + if (Notification.permission === "granted") { + return new Notification(message); + } else { + alert(message); + } +} diff --git a/registration/frontend/src/admin/onsite.tsx b/registration/frontend/src/admin/onsite.tsx index bb95f385..5336763b 100644 --- a/registration/frontend/src/admin/onsite.tsx +++ b/registration/frontend/src/admin/onsite.tsx @@ -1,49 +1,46 @@ -import { Component, createSignal, onCleanup, useContext } from "solid-js"; +import { Component, createSignal } from "solid-js"; import { render } from "solid-js/web"; import { ApisConfig } from "../entrypoints/admin"; import { ConfigContext } from "./providers/config-provider"; import { AttendeeSearch } from "./attendee-search"; -import { CartManager, CartResponse } from "./cart/cart-manager"; -import { Cart } from "./cart/components/Cart"; -import { ScanPanel } from "./scan-actions"; - -const Onsite: Component = () => { - const config = useContext(ConfigContext); - - const [cartEntries, setCartEntries] = createSignal(); - const cartManager = new CartManager(config.urls, setCartEntries); - - onCleanup(() => { - cartManager.close(); - }); +import { Cart, CartManager } from "./cart"; +import { ScanPanel } from "./scan"; +import MqttClient from "./mqtt"; +const Onsite: Component<{ mqtt: MqttClient, cartManager: CartManager }> = (props) => { const [searchQuery, setSearchQuery] = createSignal(); return (
- setSearchQuery(name)} /> + setSearchQuery(name)} + emitter={props.mqtt.emitter} + />
- +
); }; export default function createOnsiteExperience(config: ApisConfig) { + const mqtt = new MqttClient(config.mqtt); + const cartManager = new CartManager(config.urls, mqtt); + render( () => ( - + ), document.getElementById("onsite") diff --git a/registration/frontend/src/admin/scan-actions.tsx b/registration/frontend/src/admin/scan-actions.tsx deleted file mode 100644 index f30a35f4..00000000 --- a/registration/frontend/src/admin/scan-actions.tsx +++ /dev/null @@ -1,386 +0,0 @@ -import { - Component, - createEffect, - createMemo, - createSignal, - For, - JSX, - Show, - useContext, -} from "solid-js"; - -import emitter from "./mqtt"; -import { differenceInYears } from "date-fns"; -import { ConfigContext } from "./providers/config-provider"; - -interface IdData { - first: string; - last: string; - dob: string; - expiry: string; - address: string; - address2: string; - city: string; - state: string; - ZIP: string; - country: string; -} - -interface ShcData { - name: string; - birthday: string; - verification: ShcIssuer; - vaccines: ShcVaccine[]; -} - -interface ShcIssuer { - issuer: string; - verified: boolean; - trusted: boolean; -} - -interface ShcVaccine { - name: string; - lotNumber: string; - status: string; - date: string; - performer: string; -} - -interface ScanLog { - url: string | undefined; - id: IdData | undefined; - shc: ShcData | undefined; -} - -interface ShcMatch { - name: boolean; - dob: boolean; -} - -const [scanLog, setScanLog] = createSignal({ - url: undefined, - id: undefined, - shc: undefined, -}); - -export const ScanPanel: Component<{ - gotScannedName(name: string): void; -}> = (props) => { - const shcMatch = createMemo(() => { - const data = scanLog(); - if (!data.id || !data.shc) return { name: true, dob: true }; - - const idName = `${data.id.first} ${data.id.last}`; - const name = idName.localeCompare(data.shc.name) === 0; - - const dob = data.id.dob === data.shc.birthday; - - return { name, dob }; - }); - - createEffect(() => { - const id = scanLog()?.id; - if (id) { - props.gotScannedName(`${id.first} ${id.last}`); - } - }); - - const hasAnyScans = () => scanLog().id || scanLog().shc || scanLog().url; - - return ( -
-
-
-
-
Scanner Entries
- -
- -
-
-
- - -
No items scanned.
-
- - -
- -
-
- - -
- -
-
- - -
- -
-
-
-
- ); -}; - -const CloseButton: Component<{ close(): any }> = (props) => { - return ( - - ); -}; - -const MismatchedData: Component<{ - matched: boolean; - message: string; - children: JSX.Element; -}> = (props) => { - return ( - - - - - - {props.children} - - - ); -}; - -const NameBirthday: Component<{ - name: string; - birthday: string; - shcMatch?: ShcMatch; -}> = (props) => { - const age = () => { - return differenceInYears(new Date(), props.birthday); - }; - - return ( -
-
- - {props.name} - -
-
- - - - - - {`${props.birthday} (${age()} years)`} - - -
-
- ); -}; - -const UrlEntry: Component<{ url: string }> = (props) => { - return ( - - ); -}; - -const IdEntry: Component<{ data: IdData }> = (props) => { - const config = useContext(ConfigContext); - - const expirationDate = () => new Date(props.data.expiry); - const expired = () => new Date() > expirationDate(); - - const panelClasses = () => { - return { - "is-warning": expired(), - "is-success": !expired(), - }; - }; - - const regUrl = createMemo(() => { - let url = new URL(config.urls.onsite, window.location.href); - url.searchParams.set("firstName", props.data.first); - url.searchParams.set("lastName", props.data.last); - url.searchParams.set("dob", props.data.dob); - url.searchParams.set("address1", props.data.address); - if (props.data.address2) - url.searchParams.set("address2", props.data.address2); - url.searchParams.set("city", props.data.city); - url.searchParams.set("state", props.data.state); - url.searchParams.set("postalCode", props.data.ZIP.substring(0, 5)); - return url.toString(); - }); - - return ( -
-
- - - - - ID Card - - - - - - {`Expired ${props.data.expiry}`} - - - - setScanLog({ ...scanLog(), id: undefined })} - /> -
- - -
- ); -}; - -const ShcEntry: Component<{ data: ShcData; shcMatch: ShcMatch }> = (props) => { - const status = () => { - let status = "Partially Verified"; - if (props.data.verification.trusted) { - status = "Verified"; - } else if (!props.data.verification.verified) { - status = "Not Verified"; - } - return status; - }; - - return ( -
-
- - - - - Vaccination Record - {status()} - - - setScanLog({ ...scanLog(), shc: undefined })} - /> -
-
- - - - - - - - - - - - - {(vaccine, index) => { - return ( - - - - - - ); - }} - - -
DateVaccineLot
{vaccine.date}{vaccine.name}{vaccine.lotNumber}
-
-
- ); -}; - -emitter.on("open", (payload) => { - const url = payload?.["url"]; - if (!url) { - console.error("Open command missing URL"); - return; - } - - window.open(url, "link"); - setScanLog({ - ...scanLog(), - url, - }); -}); - -emitter.on("scan/id", (payload: IdData) => { - if (!payload) { - console.error("Missing ID scan payload"); - return; - } - - setScanLog({ - ...scanLog(), - id: payload, - }); -}); - -emitter.on("scan/shc", (payload: ShcData) => { - if (!payload) { - console.error("Missing SHC scan payload"); - return; - } - - setScanLog({ - ...scanLog(), - shc: payload, - }); -}); diff --git a/registration/frontend/src/admin/scan/components/CloseButton.tsx b/registration/frontend/src/admin/scan/components/CloseButton.tsx new file mode 100644 index 00000000..ff387967 --- /dev/null +++ b/registration/frontend/src/admin/scan/components/CloseButton.tsx @@ -0,0 +1,7 @@ +import { Component } from "solid-js"; + +export const CloseButton: Component<{ close(): any }> = (props) => { + return ( + + ); +}; diff --git a/registration/frontend/src/admin/scan/components/IdEntry.tsx b/registration/frontend/src/admin/scan/components/IdEntry.tsx new file mode 100644 index 00000000..ce8f8c26 --- /dev/null +++ b/registration/frontend/src/admin/scan/components/IdEntry.tsx @@ -0,0 +1,72 @@ +import { Component, createMemo, Show, useContext } from "solid-js"; + +import { ConfigContext } from "../../providers/config-provider"; +import { CloseButton } from "./CloseButton"; +import { NameBirthday } from "./ScanPii"; +import { IdData } from ".."; + +export const IdEntry: Component<{ data: IdData; remove(): void }> = (props) => { + const config = useContext(ConfigContext); + + const expirationDate = () => new Date(props.data.expiry); + const expired = () => new Date() > expirationDate(); + + const panelClasses = () => { + return { + "is-warning": expired(), + "is-success": !expired(), + }; + }; + + const regUrl = createMemo(() => { + let url = new URL(config.urls.onsite, window.location.href); + url.searchParams.set("firstName", props.data.first); + url.searchParams.set("lastName", props.data.last); + url.searchParams.set("dob", props.data.dob); + url.searchParams.set("address1", props.data.address); + if (props.data.address2) + url.searchParams.set("address2", props.data.address2); + url.searchParams.set("city", props.data.city); + url.searchParams.set("state", props.data.state); + url.searchParams.set("postalCode", props.data.ZIP.substring(0, 5)); + return url.toString(); + }); + + return ( +
+
+ + + + + ID Card + + + + + + {`Expired ${props.data.expiry}`} + + + + props.remove()} /> +
+ + +
+ ); +}; diff --git a/registration/frontend/src/admin/scan/components/MismatchedData.tsx b/registration/frontend/src/admin/scan/components/MismatchedData.tsx new file mode 100644 index 00000000..efe1499e --- /dev/null +++ b/registration/frontend/src/admin/scan/components/MismatchedData.tsx @@ -0,0 +1,18 @@ +import { Component, JSX, Show } from "solid-js"; + +export const MismatchedData: Component<{ + matched: boolean; + message: string; + children: JSX.Element; +}> = (props) => { + return ( + + + + + + {props.children} + + + ); +}; diff --git a/registration/frontend/src/admin/scan/components/ScanPanel.tsx b/registration/frontend/src/admin/scan/components/ScanPanel.tsx new file mode 100644 index 00000000..9373124e --- /dev/null +++ b/registration/frontend/src/admin/scan/components/ScanPanel.tsx @@ -0,0 +1,123 @@ +import { Component, createEffect, createMemo, Show } from "solid-js"; +import { createStore } from "solid-js/store"; + +import { IdEntry } from "./IdEntry"; +import { ShcEntry } from "./ShcEntry"; +import { UrlEntry } from "./UrlEntry"; +import { IdData, ShcData } from ".."; +import { MqttEmitter } from "../../mqtt"; + +type ScanStore = { + id?: IdData; + shc?: ShcData; + url?: string; +}; + +export const ScanPanel: Component<{ + gotScannedName(name: string): void; + emitter: MqttEmitter; +}> = (props) => { + const [store, setStore] = createStore({ + id: null, + shc: null, + url: null, + }); + + const shcMatch = createMemo(() => { + if (!store.id || !store.shc) return { name: true, dob: true }; + + const idName = `${store.id.first} ${store.id.last}`; + const name = idName.localeCompare(store.shc.name) === 0; + + const dob = store.id.dob === store.shc.birthday; + + return { name, dob }; + }); + + const hasAnyScans = () => store.id || store.shc || store.url; + + createEffect(() => { + const id = store.id; + if (id) { + props.gotScannedName(`${id.first} ${id.last}`); + } + }); + + createEffect(() => { + const emitter = props.emitter; + + emitter.on("open", (payload) => { + const url = payload?.["url"]; + if (url) { + window.open(url, "link"); + setStore("url", url); + } else { + console.error("Open command missing URL"); + } + }); + + emitter.on("scan/id", (payload: IdData) => { + if (payload) { + setStore("id", payload); + } else { + console.error("Missing ID scan payload"); + } + }); + + emitter.on("scan/shc", (payload: ShcData) => { + if (payload) { + setStore("shc", payload); + } else { + console.error("Missing SHC scan payload"); + } + }); + }); + + return ( +
+
+
+
+
Scanner Entries
+ +
+ +
+
+
+ + +
No items scanned.
+
+ + +
+ setStore("id", null)} /> +
+
+ + +
+ setStore("shc", null)} + /> +
+
+ + +
+ setStore("url", null)} /> +
+
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/scan/components/ScanPii.tsx b/registration/frontend/src/admin/scan/components/ScanPii.tsx new file mode 100644 index 00000000..6a8692b1 --- /dev/null +++ b/registration/frontend/src/admin/scan/components/ScanPii.tsx @@ -0,0 +1,41 @@ +import { differenceInYears } from "date-fns/differenceInYears"; +import { Component } from "solid-js"; + +import { MismatchedData } from "./MismatchedData"; +import { ShcMatch } from ".."; + +export const NameBirthday: Component<{ + name: string; + birthday: string; + shcMatch?: ShcMatch; +}> = (props) => { + const age = () => { + return differenceInYears(new Date(), props.birthday); + }; + + return ( +
+
+ + {props.name} + +
+
+ + + + + + {`${props.birthday} (${age()} years)`} + + +
+
+ ); +}; diff --git a/registration/frontend/src/admin/scan/components/ShcEntry.tsx b/registration/frontend/src/admin/scan/components/ShcEntry.tsx new file mode 100644 index 00000000..87a491df --- /dev/null +++ b/registration/frontend/src/admin/scan/components/ShcEntry.tsx @@ -0,0 +1,73 @@ +import { Component, For } from "solid-js"; + +import { CloseButton } from "./CloseButton"; +import { NameBirthday } from "./ScanPii"; +import { ShcData, ShcMatch } from ".."; + +export const ShcEntry: Component<{ + data: ShcData; + shcMatch: ShcMatch; + remove(): void; +}> = (props) => { + const status = () => { + let status = "Partially Verified"; + if (props.data.verification.trusted) { + status = "Verified"; + } else if (!props.data.verification.verified) { + status = "Not Verified"; + } + return status; + }; + + return ( +
+
+ + + + + Vaccination Record - {status()} + + + props.remove()} /> +
+
+ + + + + + + + + + + + + {(vaccine, index) => { + return ( + + + + + + ); + }} + + +
DateVaccineLot
{vaccine.date}{vaccine.name}{vaccine.lotNumber}
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/scan/components/UrlEntry.tsx b/registration/frontend/src/admin/scan/components/UrlEntry.tsx new file mode 100644 index 00000000..9cc08bf1 --- /dev/null +++ b/registration/frontend/src/admin/scan/components/UrlEntry.tsx @@ -0,0 +1,20 @@ +import { Component } from "solid-js"; + +import { CloseButton } from "./CloseButton"; + +export const UrlEntry: Component<{ url: string; remove(): void }> = (props) => { + return ( + + ); +}; diff --git a/registration/frontend/src/admin/scan/index.ts b/registration/frontend/src/admin/scan/index.ts new file mode 100644 index 00000000..beb3ea45 --- /dev/null +++ b/registration/frontend/src/admin/scan/index.ts @@ -0,0 +1,57 @@ +import { createSignal } from "solid-js"; + +import emitter from "../mqtt"; +import { ScanPanel } from "./components/ScanPanel"; + +export { ScanPanel }; + +export interface IdData { + first: string; + last: string; + dob: string; + expiry: string; + address: string; + address2: string; + city: string; + state: string; + ZIP: string; + country: string; +} + +export interface ShcData { + name: string; + birthday: string; + verification: ShcIssuer; + vaccines: ShcVaccine[]; +} + +export interface ShcIssuer { + issuer: string; + verified: boolean; + trusted: boolean; +} + +export interface ShcVaccine { + name: string; + lotNumber: string; + status: string; + date: string; + performer: string; +} + +export interface ScanLog { + url: string | undefined; + id: IdData | undefined; + shc: ShcData | undefined; +} + +export interface ShcMatch { + name: boolean; + dob: boolean; +} + +const [scanLog, setScanLog] = createSignal({ + url: undefined, + id: undefined, + shc: undefined, +}); diff --git a/registration/frontend/src/entrypoints/admin.ts b/registration/frontend/src/entrypoints/admin.ts index 9384a8c3..8d7cfba6 100644 --- a/registration/frontend/src/entrypoints/admin.ts +++ b/registration/frontend/src/entrypoints/admin.ts @@ -1,5 +1,3 @@ -import "../admin/scan-actions"; -import { connectToMqtt } from "../admin/mqtt"; import createActions from "../admin/navbar"; import createOnsiteExperience from "../admin/onsite"; @@ -74,9 +72,5 @@ declare global { const APIS_CONFIG: ApisConfig; } -if (APIS_CONFIG.mqtt) { - connectToMqtt(APIS_CONFIG.mqtt) -} - createActions(APIS_CONFIG); createOnsiteExperience(APIS_CONFIG); From 15d284dbb29a8ff3cac7f9889217ac1f23dae0b6 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Fri, 8 Nov 2024 14:08:23 -0500 Subject: [PATCH 08/44] Cleanup and layout improvements. --- .../components/AttendeeSearch.tsx | 8 +- .../components/BadgeTableLoader.tsx | 2 +- .../components/BadgeTableRow.tsx | 2 +- .../frontend/src/admin/cart/cart-manager.ts | 1 - .../admin/cart/components/ActionButton.tsx | 57 ++++---- .../src/admin/cart/components/Cart.tsx | 11 +- .../src/admin/cart/components/CartActions.tsx | 134 ++++++++++-------- .../cart/components/CartActionsError.tsx | 26 ++++ .../src/admin/cart/components/CartEntries.tsx | 4 +- registration/frontend/src/admin/index.scss | 4 - registration/frontend/src/admin/mqtt.ts | 2 +- registration/frontend/src/admin/navbar.tsx | 15 +- registration/frontend/src/admin/onsite.tsx | 6 +- .../src/admin/scan/components/IdEntry.tsx | 4 +- .../src/admin/scan/components/ScanPanel.tsx | 6 +- .../src/admin/scan/components/ScanPii.tsx | 2 +- registration/frontend/src/admin/scan/index.ts | 1 - .../frontend/src/entrypoints/admin.ts | 4 +- 18 files changed, 171 insertions(+), 118 deletions(-) create mode 100644 registration/frontend/src/admin/cart/components/CartActionsError.tsx diff --git a/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx b/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx index 8c78b88d..befb3eb4 100644 --- a/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx +++ b/registration/frontend/src/admin/attendee-search/components/AttendeeSearch.tsx @@ -9,11 +9,11 @@ import { useContext, } from "solid-js"; -import { ConfigContext } from "../../providers/config-provider"; +import { BadgeTableLoader } from "./BadgeTableLoader"; +import { BadgeTableRow } from "./BadgeTableRow"; import { CartManager } from "../../cart"; +import { ConfigContext } from "../../providers/config-provider"; import { getSearchResults } from ".."; -import { BadgeTableRow } from "./BadgeTableRow"; -import { BadgeTableLoader } from "./BadgeTableLoader"; export const AttendeeSearch: Component<{ cartManager: CartManager; @@ -47,7 +47,7 @@ export const AttendeeSearch: Component<{
-
+
Attendee Search
diff --git a/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx b/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx index 15f8a6da..c8210fe7 100644 --- a/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx +++ b/registration/frontend/src/admin/attendee-search/components/BadgeTableLoader.tsx @@ -13,7 +13,7 @@ const Row: Component = () => {
-
Full Name
+
Longer Full Name
diff --git a/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx b/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx index 415da85e..b6bdb3ca 100644 --- a/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx +++ b/registration/frontend/src/admin/attendee-search/components/BadgeTableRow.tsx @@ -1,7 +1,7 @@ import { Component, Show } from "solid-js"; -import { CartManager } from "../../cart"; import { BadgeResult } from ".."; +import { CartManager } from "../../cart"; export const BadgeTableRow: Component<{ cartManager: CartManager; diff --git a/registration/frontend/src/admin/cart/cart-manager.ts b/registration/frontend/src/admin/cart/cart-manager.ts index 9f0409bd..0d2a880a 100644 --- a/registration/frontend/src/admin/cart/cart-manager.ts +++ b/registration/frontend/src/admin/cart/cart-manager.ts @@ -1,7 +1,6 @@ import { Accessor, createSignal, Setter } from "solid-js"; import { ApisUrls, CSRF_TOKEN } from "../../entrypoints/admin"; -import emitter from "../mqtt"; import MqttClient from "../mqtt"; export class CartManager { diff --git a/registration/frontend/src/admin/cart/components/ActionButton.tsx b/registration/frontend/src/admin/cart/components/ActionButton.tsx index 0f59b5ea..953a9c3b 100644 --- a/registration/frontend/src/admin/cart/components/ActionButton.tsx +++ b/registration/frontend/src/admin/cart/components/ActionButton.tsx @@ -1,32 +1,48 @@ -import { Component, JSX, Setter } from "solid-js"; - -export type ActionButtonKey = "cash" | "card" | "print"; +import { + Component, + createEffect, + createResource, + createSignal, + JSX, + Setter, +} from "solid-js"; export type ActionButtonProps = { - button: ActionButtonKey; class: string; disabled: boolean; - loadingButton?: ActionButtonKey; - setLoadingButton: Setter; + loading: boolean; + setLoading: Setter; action: (ev: MouseEvent) => Promise; children: JSX.Element; }; export const ActionButton: Component = (props) => { - let classes = `button is-fullwidth ${props.class}`; + const [triggerEvent, setTriggerEvent] = createSignal(); + const [resource] = createResource(triggerEvent, async (ev) => { + console.debug("Attempting to perform button action"); + props.setLoading(true); + const resp = await props.action(ev); + props.setLoading(false); + return resp; + }); + + createEffect(() => { + // When we have an error, throw it up. All of the buttons are wrapped in an + // error boundary so an appropriately sized message can display. + const err = resource.error; + if (err) { + throw err; + } + }); return (
); }; - -async function trackLoadingButton( - setLoadingButton: Setter, - button: ActionButtonKey, - action: Promise -): Promise { - setLoadingButton(button); - const resp = await action; - setLoadingButton(null); - return resp; -} diff --git a/registration/frontend/src/admin/cart/components/Cart.tsx b/registration/frontend/src/admin/cart/components/Cart.tsx index 621a8eb6..efa5a5d1 100644 --- a/registration/frontend/src/admin/cart/components/Cart.tsx +++ b/registration/frontend/src/admin/cart/components/Cart.tsx @@ -1,9 +1,9 @@ import { Show } from "solid-js/web"; import { Component, createEffect, createResource } from "solid-js"; -import { CartManager, CartResponse } from "../cart-manager"; -import { CartEntries } from "./CartEntries"; import { CartActions } from "./CartActions"; +import { CartEntries } from "./CartEntries"; +import { CartManager } from "../cart-manager"; export const Cart: Component<{ cartManager: CartManager; @@ -25,7 +25,7 @@ export const Cart: Component<{ return (
-
+
Cart
@@ -65,7 +65,10 @@ export const Cart: Component<{
- +
); diff --git a/registration/frontend/src/admin/cart/components/CartActions.tsx b/registration/frontend/src/admin/cart/components/CartActions.tsx index f33bff1f..a0e5daa3 100644 --- a/registration/frontend/src/admin/cart/components/CartActions.tsx +++ b/registration/frontend/src/admin/cart/components/CartActions.tsx @@ -1,9 +1,16 @@ -import { Component, createMemo, createSignal, useContext } from "solid-js"; +import { + Component, + createMemo, + createSignal, + ErrorBoundary, + useContext, +} from "solid-js"; import { Big } from "big.js"; +import { ActionButton } from "./ActionButton"; import { CartManager, CartResponse } from "../cart-manager"; import { ConfigContext } from "../../providers/config-provider"; -import { ActionButton, ActionButtonKey } from "./ActionButton"; +import { CartActionsError } from "./CartActionsError"; const PRINTABLE_STATUS = new Set(["Paid", "Comp", "Staff", "Dealer"]); @@ -13,7 +20,7 @@ export const CartActions: Component<{ }> = (props) => { const config = useContext(ConfigContext); - const [loadingButton, setLoadingButton] = createSignal(); + const [loading, setLoading] = createSignal(false); const hasHold = createMemo( () => @@ -50,61 +57,70 @@ export const CartActions: Component<{ return (
-
- - attemptCashPayment( - props.manager, - props.cartEntries.reference, - props.cartEntries.total - ) - } - > - - - - Tender Cash - - enableCardPayment(props.manager)} - > - - - - Credit/Debit Card - -
-
- - printBadges( - props.manager, - printableBadgeIds(), - config.mqtt.supports_printing && !ev.shiftKey - ) - } - > - - - - Print Badges - -
+ ( + { + setLoading(undefined); + reset(); + }} + /> + )} + > +
+ + attemptCashPayment( + props.manager, + props.cartEntries.reference, + props.cartEntries.total + ) + } + > + + + + Tender Cash + + enableCardPayment(props.manager)} + > + + + + Credit/Debit Card + +
+
+ + printBadges( + props.manager, + printableBadgeIds(), + config.mqtt.supports_printing && !ev.shiftKey + ) + } + > + + + + Print Badges + +
+
); }; @@ -161,7 +177,7 @@ async function printBadges( return; } - console.debug(`Got response from badge print: mqttPrint=${mqttPrint}, resp=${resp}`); + console.debug("Got response from badge print", mqttPrint, resp); if (!mqttPrint) { window.open(resp.url, "badge"); diff --git a/registration/frontend/src/admin/cart/components/CartActionsError.tsx b/registration/frontend/src/admin/cart/components/CartActionsError.tsx new file mode 100644 index 00000000..2b48882f --- /dev/null +++ b/registration/frontend/src/admin/cart/components/CartActionsError.tsx @@ -0,0 +1,26 @@ +import { Component, createEffect, Show } from "solid-js"; + +export const CartActionsError: Component<{ err: any; reset(): void }> = ( + props +) => { + createEffect(() => { + console.error("Cart had error", props.err); + }); + + return ( +
+
+

Cart Error

+ + +
+ + 0} + fallback={
An unknown error occured.
} + > +
{props.err.toString()}
+
+
+ ); +}; diff --git a/registration/frontend/src/admin/cart/components/CartEntries.tsx b/registration/frontend/src/admin/cart/components/CartEntries.tsx index d512a929..54e2b735 100644 --- a/registration/frontend/src/admin/cart/components/CartEntries.tsx +++ b/registration/frontend/src/admin/cart/components/CartEntries.tsx @@ -1,8 +1,8 @@ import { Component, createMemo, For, Show } from "solid-js"; import { Big } from "big.js"; -import { CartManager, CartResponse } from "../cart-manager"; import { CartBadge } from "./CartBadge"; +import { CartManager, CartResponse } from "../cart-manager"; export const CartEntries: Component<{ manager: CartManager; @@ -84,7 +84,7 @@ export const CartEntries: Component<{ 0}> -
+
{(badge, index) => ( = (props) => { const Navbar: Component = () => { const config = useContext(ConfigContext); + const [active, setActive] = createSignal(false); + return (
); From e6a1f46a0e3ac80b39f0f0c3dba81c60bd5ca51e Mon Sep 17 00:00:00 2001 From: Syfaro Date: Fri, 8 Nov 2024 21:37:54 -0500 Subject: [PATCH 15/44] More minor fixes. --- registration/frontend/src/admin/{navbar.tsx => Navbar.tsx} | 0 registration/frontend/src/admin/{onsite.tsx => Onsite.tsx} | 0 registration/frontend/src/admin/cart/components/CartBadge.tsx | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename registration/frontend/src/admin/{navbar.tsx => Navbar.tsx} (100%) rename registration/frontend/src/admin/{onsite.tsx => Onsite.tsx} (100%) diff --git a/registration/frontend/src/admin/navbar.tsx b/registration/frontend/src/admin/Navbar.tsx similarity index 100% rename from registration/frontend/src/admin/navbar.tsx rename to registration/frontend/src/admin/Navbar.tsx diff --git a/registration/frontend/src/admin/onsite.tsx b/registration/frontend/src/admin/Onsite.tsx similarity index 100% rename from registration/frontend/src/admin/onsite.tsx rename to registration/frontend/src/admin/Onsite.tsx diff --git a/registration/frontend/src/admin/cart/components/CartBadge.tsx b/registration/frontend/src/admin/cart/components/CartBadge.tsx index 5e6b9989..ff80bcf8 100644 --- a/registration/frontend/src/admin/cart/components/CartBadge.tsx +++ b/registration/frontend/src/admin/cart/components/CartBadge.tsx @@ -10,7 +10,7 @@ export const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( const [resource] = createResource(clearBadgeId, async (id) => { const resp = await props.manager.clearBadgePrinted(id); if (resp.success) { - await props.manager.printBadges([id]); + await props.manager.refreshCart(); } else { alert("Unable to clear badge print flag."); } @@ -43,7 +43,7 @@ export const CartBadge: Component<{ manager: CartManager; badge: Badge }> = ( title="Already printed" onClick={() => { if ( - confirm("Are you sure you need to re-print this badge?") + confirm("Are you sure you need to clear the print flag for this badge?") ) { setClearBadgeId(props.badge.id); } From 6dca41d4c0af1fad1daf08a46b52964e1684171d Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sat, 9 Nov 2024 00:39:20 -0500 Subject: [PATCH 16/44] More improvements. --- registration/frontend/package-lock.json | 206 ++++++++++++++++++ registration/frontend/package.json | 1 + registration/frontend/src/admin/Navbar.tsx | 99 ++++++++- .../components/AttendeeSearch.tsx | 14 +- .../components/BadgeTableRow.tsx | 26 ++- .../admin/cart/components/ActionButton.tsx | 1 - .../src/admin/cart/components/Cart.tsx | 57 +++-- .../src/admin/cart/components/CartActions.tsx | 2 - registration/frontend/src/admin/index.scss | 2 +- registration/frontend/src/admin/mqtt.ts | 12 +- .../src/admin/scan/components/IdEntry.tsx | 18 +- .../src/admin/scan/components/ScanPanel.tsx | 5 +- .../templates/registration/onsite-admin.html | 2 +- registration/views/onsite_admin.py | 4 +- 14 files changed, 401 insertions(+), 48 deletions(-) diff --git a/registration/frontend/package-lock.json b/registration/frontend/package-lock.json index 4a15caeb..f30bb813 100644 --- a/registration/frontend/package-lock.json +++ b/registration/frontend/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "@fortawesome/fontawesome-free": "^6.6.0", + "@kobalte/core": "^0.13.7", "@solid-primitives/keyboard": "^1.2.8", "big.js": "^6.2.2", "bulma": "^1.0.2", @@ -471,6 +472,17 @@ "integrity": "sha512-UNtPCbrwrenpmrXuRwn9jYpPoweNXj8X5sMvYgsqYyaH8jQ6LfUJSk3dJLnBK+6sfYPrF4iAIo5sd5HQ+tg75A==", "peer": true }, + "node_modules/@corvu/utils": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@corvu/utils/-/utils-0.4.2.tgz", + "integrity": "sha512-Ox2kYyxy7NoXdKWdHeDEjZxClwzO4SKM8plAaVwmAJPxHMqA0rLOoAsa+hBDwRLpctf+ZRnAd/ykguuJidnaTA==", + "dependencies": { + "@floating-ui/dom": "^1.6.11" + }, + "peerDependencies": { + "solid-js": "^1.8" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", @@ -855,6 +867,28 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@fortawesome/fontawesome-free": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", @@ -863,6 +897,22 @@ "node": ">=6" } }, + "node_modules/@internationalized/date": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.6.tgz", + "integrity": "sha512-jLxQjefH9VI5P9UQuqB6qNKnvFt1Ky1TPIzHGsIlCi7sZZoMR8SdYbBGRvM0y+Jtb+ez4ieBzmiAUcpmPYpyOw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.5.4.tgz", + "integrity": "sha512-h9huwWjNqYyE2FXZZewWqmCdkw1HeFds5q4Siuoms3hUQC5iPJK3aBmkFZoDSLN4UD0Bl8G22L/NdHpeOr+/7A==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -911,6 +961,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kobalte/core": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@kobalte/core/-/core-0.13.7.tgz", + "integrity": "sha512-COhjWk1KnCkl3qMJDvdrOsvpTlJ9gMLdemkAn5SWfbPn/lxJYabejnNOk+b/ILGg7apzQycgbuo48qb8ppqsAg==", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "@internationalized/date": "^3.4.0", + "@internationalized/number": "^3.2.1", + "@kobalte/utils": "^0.9.1", + "@solid-primitives/props": "^3.1.8", + "@solid-primitives/resize-observer": "^2.0.26", + "solid-presence": "^0.1.8", + "solid-prevent-scroll": "^0.1.4" + }, + "peerDependencies": { + "solid-js": "^1.8.15" + } + }, + "node_modules/@kobalte/utils": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@kobalte/utils/-/utils-0.9.1.tgz", + "integrity": "sha512-eeU60A3kprIiBDAfv9gUJX1tXGLuZiKMajUfSQURAF2pk4ZoMYiqIzmrMBvzcxP39xnYttgTyQEVLwiTZnrV4w==", + "dependencies": { + "@solid-primitives/event-listener": "^2.2.14", + "@solid-primitives/keyed": "^1.2.0", + "@solid-primitives/map": "^0.4.7", + "@solid-primitives/media": "^2.2.4", + "@solid-primitives/props": "^3.1.8", + "@solid-primitives/refs": "^1.0.5", + "@solid-primitives/utils": "^6.2.1" + }, + "peerDependencies": { + "solid-js": "^1.8.8" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", @@ -1217,6 +1302,75 @@ "solid-js": "^1.6.12" } }, + "node_modules/@solid-primitives/keyed": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@solid-primitives/keyed/-/keyed-1.2.3.tgz", + "integrity": "sha512-Tlm2wCKcXEVxqd1speWjPhGvDhuuo/VeWSvNF6r2h77BUOHRKmNwz9uVKKMQmYSaLwiptJTp+fPZY2dOVPWQRQ==", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/map": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@solid-primitives/map/-/map-0.4.13.tgz", + "integrity": "sha512-B1zyFbsiTQvqPr+cuPCXO72sRuczG9Swncqk5P74NCGw1VE8qa/Ry9GlfI1e/VdeQYHjan+XkbE3rO2GW/qKew==", + "dependencies": { + "@solid-primitives/trigger": "^1.1.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/media": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@solid-primitives/media/-/media-2.2.9.tgz", + "integrity": "sha512-QUmU62D4/d9YWx/4Dvr/UZasIkIpqNXz7wosA5GLmesRW9XlPa3G5M6uOmTw73SByHNTCw0D6x8bSdtvvLgzvQ==", + "dependencies": { + "@solid-primitives/event-listener": "^2.3.3", + "@solid-primitives/rootless": "^1.4.5", + "@solid-primitives/static-store": "^0.0.8", + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/props": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@solid-primitives/props/-/props-3.1.11.tgz", + "integrity": "sha512-jZAKWwvDRHjiydIumDgMj68qviIbowQ1ci7nkEAgzgvanNkhKSQV8iPgR2jMk1uv7S2ZqXYHslVQTgJel/TEyg==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/refs": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.0.8.tgz", + "integrity": "sha512-+jIsWG8/nYvhaCoG2Vg6CJOLgTmPKFbaCrNQKWfChalgUf9WrVxWw0CdJb3yX15n5lUcQ0jBo6qYtuVVmBLpBw==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/resize-observer": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/@solid-primitives/resize-observer/-/resize-observer-2.0.26.tgz", + "integrity": "sha512-KbPhwal6ML9OHeUTZszBbt6PYSMj89d4wVCLxlvDYL4U0+p+xlCEaqz6v9dkCwm/0Lb+Wed7W5T1dQZCP3JUUw==", + "dependencies": { + "@solid-primitives/event-listener": "^2.3.3", + "@solid-primitives/rootless": "^1.4.5", + "@solid-primitives/static-store": "^0.0.8", + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, "node_modules/@solid-primitives/rootless": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@solid-primitives/rootless/-/rootless-1.4.5.tgz", @@ -1228,6 +1382,28 @@ "solid-js": "^1.6.12" } }, + "node_modules/@solid-primitives/static-store": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@solid-primitives/static-store/-/static-store-0.0.8.tgz", + "integrity": "sha512-ZecE4BqY0oBk0YG00nzaAWO5Mjcny8Fc06CdbXadH9T9lzq/9GefqcSe/5AtdXqjvY/DtJ5C6CkcjPZO0o/eqg==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/trigger": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@solid-primitives/trigger/-/trigger-1.1.0.tgz", + "integrity": "sha512-00BbAiXV66WwjHuKZc3wr0+GLb9C24mMUmi3JdTpNFgHBbrQGrIHubmZDg36c5/7wH+E0GQtOOanwQS063PO+A==", + "dependencies": { + "@solid-primitives/utils": "^6.2.3" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, "node_modules/@solid-primitives/utils": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz", @@ -1236,6 +1412,14 @@ "solid-js": "^1.6.12" } }, + "node_modules/@swc/helpers": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/big.js": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", @@ -2550,6 +2734,28 @@ "seroval-plugins": "^1.1.0" } }, + "node_modules/solid-presence": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/solid-presence/-/solid-presence-0.1.8.tgz", + "integrity": "sha512-pWGtXUFWYYUZNbg5YpG5vkQJyOtzn2KXhxYaMx/4I+lylTLYkITOLevaCwMRN+liCVk0pqB6EayLWojNqBFECA==", + "dependencies": { + "@corvu/utils": "~0.4.0" + }, + "peerDependencies": { + "solid-js": "^1.8" + } + }, + "node_modules/solid-prevent-scroll": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/solid-prevent-scroll/-/solid-prevent-scroll-0.1.10.tgz", + "integrity": "sha512-KplGPX2GHiWJLZ6AXYRql4M127PdYzfwvLJJXMkO+CMb8Np4VxqDAg5S8jLdwlEuBis/ia9DKw2M8dFx5u8Mhw==", + "dependencies": { + "@corvu/utils": "~0.4.1" + }, + "peerDependencies": { + "solid-js": "^1.8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/registration/frontend/package.json b/registration/frontend/package.json index f10a6f5a..b46fb8e2 100644 --- a/registration/frontend/package.json +++ b/registration/frontend/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@fortawesome/fontawesome-free": "^6.6.0", + "@kobalte/core": "^0.13.7", "@solid-primitives/keyboard": "^1.2.8", "big.js": "^6.2.2", "bulma": "^1.0.2", diff --git a/registration/frontend/src/admin/Navbar.tsx b/registration/frontend/src/admin/Navbar.tsx index 5ab61b98..f2a369c7 100644 --- a/registration/frontend/src/admin/Navbar.tsx +++ b/registration/frontend/src/admin/Navbar.tsx @@ -1,10 +1,34 @@ +import { ApisConfig, CSRF_TOKEN } from "../entrypoints/admin"; import { Big } from "big.js"; -import { Component, createSignal, For, Show, useContext } from "solid-js"; +import { + Component, + createEffect, + createSignal, + For, + Show, + useContext, +} from "solid-js"; +import { ConfigContext } from "./providers/config-provider"; import { createShortcut, KbdKey } from "@solid-primitives/keyboard"; +import { Dialog } from "@kobalte/core/dialog"; import { render } from "solid-js/web"; -import { ApisConfig, CSRF_TOKEN } from "../entrypoints/admin"; -import { ConfigContext } from "./providers/config-provider"; +const KNOWN_SHORTCUTS = [ + { shortcut: "Alt+O", description: "Open position" }, + { shortcut: "Alt+L", description: "Close position" }, + { shortcut: "Alt+N", description: "Ready for next" }, + { shortcut: "Ctrl+N", description: "Create new attendee" }, + { shortcut: "Ctrl+M", description: "Create new attendee from scanned ID" }, + { shortcut: "Alt+F", description: "Clear results and focus search field" }, + { shortcut: "Alt+S", description: "Clear scanner entries" }, + { shortcut: "Alt+A", description: "Clear cart" }, + { shortcut: "Alt+R", description: "Reload cart" }, + { shortcut: "Alt+.", description: "Add first badge search result to cart" }, + { shortcut: "Alt+\\", description: "Remove last badge from cart" }, + { shortcut: "Alt+M", description: "Tender cash payment" }, + { shortcut: "Alt+C", description: "Prompt for card payment" }, + { shortcut: "Ctrl+P", description: "Print badges in cart" }, +]; const ActionButton: Component<{ name: string; @@ -174,6 +198,25 @@ const Actions: Component<{ config: ApisConfig }> = (props) => { const Navbar: Component = () => { const config = useContext(ConfigContext); + function switchTerminal(value: string) { + console.debug(`Switching to terminal ${value}`); + let url = new URL(window.location.href); + url.searchParams.set("terminal", value); + window.location.href = url.toString(); + } + + createEffect(() => { + const availableIds = config.terminals.available.map( + (terminal) => terminal.id + ); + if ( + availableIds.length > 0 && + !availableIds.includes(config.terminals.selected) + ) { + switchTerminal(availableIds[0].toString()); + } + }); + const [active, setActive] = createSignal(false); return ( @@ -203,14 +246,10 @@ const Navbar: Component = () => {