INIT frontend

This commit is contained in:
2025-11-28 11:55:01 -06:00
parent f99d21805f
commit 5029278228
31 changed files with 3766 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
+15
View File
@@ -0,0 +1,15 @@
# job-info
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
+240
View File
@@ -0,0 +1,240 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "job-info",
"dependencies": {
"@azure/msal-browser": "^4.26.1",
"@fluentui/react-components": "9.72.7",
"hono": "^4.10.4",
"pocketbase": "^0.26.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.6.3",
},
},
},
"packages": {
"@azure/msal-browser": ["@azure/msal-browser@4.26.1", "", { "dependencies": { "@azure/msal-common": "15.13.1" } }, "sha512-GGCIsZXxyNm5QcQZ4maA9q+9UWmM+/87G+ybvPkrE32el1URSa9WYt0t67ks3/P0gspZX9RoEqyLqJ/X/JDnBQ=="],
"@azure/msal-common": ["@azure/msal-common@15.13.1", "", {}, "sha512-vQYQcG4J43UWgo1lj7LcmdsGUKWYo28RfEvDQAEMmQIMjSFufvb+pS0FJ3KXmrPmnWlt1vHDl3oip6mIDUQ4uA=="],
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@ctrl/tinycolor": ["@ctrl/tinycolor@3.6.1", "", {}, "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA=="],
"@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/devtools": ["@floating-ui/devtools@0.2.3", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0" } }, "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@fluentui/keyboard-keys": ["@fluentui/keyboard-keys@9.0.8", "", { "dependencies": { "@swc/helpers": "^0.5.1" } }, "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw=="],
"@fluentui/priority-overflow": ["@fluentui/priority-overflow@9.2.1", "", { "dependencies": { "@swc/helpers": "^0.5.1" } }, "sha512-WH5dv54aEqWo/kKQuADAwjv66W6OUMFllQMjpdkrktQp7pu4JXtmF60iYcp9+iuIX9iCeW01j8gNTU08MQlfIQ=="],
"@fluentui/react-accordion": ["@fluentui/react-accordion@9.8.14", "", { "dependencies": { "@fluentui/react-aria": "^9.17.6", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-jTcfYDRUotRhUEjE1LeG1Qm10515CQUKxHWQhppBYhq7yAZcS5jOms5tMZHtHs0EQsWv3nMgUYYqoOqAsU0jDQ=="],
"@fluentui/react-alert": ["@fluentui/react-alert@9.0.0-beta.129", "", { "dependencies": { "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-button": "^9.6.12", "@fluentui/react-icons": "^2.0.239", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-afS5Mvf9EH5je3ZOnF96GNaXL5nA/eI69AhO4nsbsvc1RaO/CkEt9+6iVyGy2zeqbQgpsP9UkNwEYyToQ1CrzA=="],
"@fluentui/react-aria": ["@fluentui/react-aria@9.17.6", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-utilities": "^9.25.4", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-O421keKMgf9BkHH15kTnKGFuCFKN3ukydJLEfSQJmOfdAHyJMzAul8/zMvkd4vmMr84+PtZUD1+Tylk4NvpN4g=="],
"@fluentui/react-avatar": ["@fluentui/react-avatar@9.9.12", "", { "dependencies": { "@fluentui/react-badge": "^9.4.11", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-popover": "^9.12.12", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-tooltip": "^9.8.11", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-dlJ5mOKCDChMAECFhpcPHoQicA28ATWQXLtz26hAuVJH2/gC/6mZ0j7drIVl9YECqT/ZbZ3/hpVeZu/S/FVrOA=="],
"@fluentui/react-badge": ["@fluentui/react-badge@9.4.11", "", { "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-u2gTg+QeD5uaieAwE89n8MLg2MyZN/kGMx3hJewFKtq3SzvU4xcgcna2Gp4UgpaA3pnGZsJjjjDIHwsv4EyO9Q=="],
"@fluentui/react-breadcrumb": ["@fluentui/react-breadcrumb@9.3.12", "", { "dependencies": { "@fluentui/react-aria": "^9.17.6", "@fluentui/react-button": "^9.6.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-link": "^9.7.0", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-cT5xmYQbAYH7HslJu6O5WvSYzsBvaQ54Q6yIPgV5kCo5n3M6OSrJ0Ga6Zbfqid/GnY4G60FfjOvbfHNNhmx2Sw=="],
"@fluentui/react-button": ["@fluentui/react-button@9.6.12", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-seI9L9O0fCHzlfKD/via1qqzaLFeiFKQeR1/97nXL06reC3DqLSCeiZP3UTxFljFE1CYZQRJfk1wH/D6j0ZCTA=="],
"@fluentui/react-card": ["@fluentui/react-card@9.5.6", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-text": "^9.6.11", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-hCY6VWrKqq+y0yqUkqgkpTN5TVJSU5ZlKtZU+Sed+TlnKlojkS6cYRvsnWdAKwyFLJF9ZYTn+uos9Vi0wQyjtg=="],
"@fluentui/react-carousel": ["@fluentui/react-carousel@9.8.12", "", { "dependencies": { "@fluentui/react-aria": "^9.17.6", "@fluentui/react-button": "^9.6.12", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-tooltip": "^9.8.11", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "embla-carousel": "^8.5.1", "embla-carousel-autoplay": "^8.5.1", "embla-carousel-fade": "^8.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-Gjn6cd67FodcjfU2MQTBI2xjijzgy54TdQA8vxObZ27I6y9OHeDR07PWTqaCkX8mcBR8ilTxVD5bQ+zuqfb66g=="],
"@fluentui/react-checkbox": ["@fluentui/react-checkbox@9.5.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-M8DTBQK0Z7+HKfRx4mjypH0fEagKK7YMNhGMy18aW3iYWeooA0ut81MzsRM5feqhl+Q8v4VJ0aN9qHNqshkD5g=="],
"@fluentui/react-color-picker": ["@fluentui/react-color-picker@9.2.11", "", { "dependencies": { "@ctrl/tinycolor": "^3.3.4", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-L1ZKJAyioey3glmzMrpawUrzsdu/Nz0m6nVMOznJVuw0vu0BfQuMh/1/0QOoGYXFEbsc4+gSGSCnah4X0EJIsQ=="],
"@fluentui/react-combobox": ["@fluentui/react-combobox@9.16.12", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-SimZpXzTGyDAGHQZmzUl9AsrIOlLDinTbvEwELEYh9X+yE33SZatcPwdpCmBXldBOs/eh+xOuNSOwgerJ3T3qQ=="],
"@fluentui/react-components": ["@fluentui/react-components@9.72.7", "", { "dependencies": { "@fluentui/react-accordion": "^9.8.14", "@fluentui/react-alert": "9.0.0-beta.129", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-badge": "^9.4.11", "@fluentui/react-breadcrumb": "^9.3.12", "@fluentui/react-button": "^9.6.12", "@fluentui/react-card": "^9.5.6", "@fluentui/react-carousel": "^9.8.12", "@fluentui/react-checkbox": "^9.5.11", "@fluentui/react-color-picker": "^9.2.11", "@fluentui/react-combobox": "^9.16.12", "@fluentui/react-dialog": "^9.16.3", "@fluentui/react-divider": "^9.4.11", "@fluentui/react-drawer": "^9.10.9", "@fluentui/react-field": "^9.4.11", "@fluentui/react-image": "^9.3.11", "@fluentui/react-infobutton": "9.0.0-beta.107", "@fluentui/react-infolabel": "^9.4.12", "@fluentui/react-input": "^9.7.11", "@fluentui/react-label": "^9.3.11", "@fluentui/react-link": "^9.7.0", "@fluentui/react-list": "^9.6.6", "@fluentui/react-menu": "^9.20.5", "@fluentui/react-message-bar": "^9.6.14", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-nav": "^9.3.14", "@fluentui/react-overflow": "^9.6.5", "@fluentui/react-persona": "^9.5.12", "@fluentui/react-popover": "^9.12.12", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-progress": "^9.4.11", "@fluentui/react-provider": "^9.22.11", "@fluentui/react-radio": "^9.5.11", "@fluentui/react-rating": "^9.3.11", "@fluentui/react-search": "^9.3.11", "@fluentui/react-select": "^9.4.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-skeleton": "^9.4.11", "@fluentui/react-slider": "^9.5.11", "@fluentui/react-spinbutton": "^9.5.11", "@fluentui/react-spinner": "^9.7.11", "@fluentui/react-swatch-picker": "^9.4.11", "@fluentui/react-switch": "^9.4.11", "@fluentui/react-table": "^9.19.5", "@fluentui/react-tabs": "^9.10.7", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-tag-picker": "^9.7.12", "@fluentui/react-tags": "^9.7.12", "@fluentui/react-teaching-popover": "^9.6.12", "@fluentui/react-text": "^9.6.11", "@fluentui/react-textarea": "^9.6.11", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-toast": "^9.7.9", "@fluentui/react-toolbar": "^9.6.12", "@fluentui/react-tooltip": "^9.8.11", "@fluentui/react-tree": "^9.15.6", "@fluentui/react-utilities": "^9.25.4", "@fluentui/react-virtualizer": "9.0.0-alpha.107", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-tuC8ZMBQicF4p+f9MJv9cVYZUSktQVreAGJq/YJxQ0Ts1mO2rnAuIBkBFlgjnjyebDiAO1FoAAz/wW99hrIh6A=="],
"@fluentui/react-context-selector": ["@fluentui/react-context-selector@9.2.12", "", { "dependencies": { "@fluentui/react-utilities": "^9.25.4", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0", "scheduler": ">=0.19.0" } }, "sha512-4hj+rv+4Uwn9EeDyXD1YCEpVkm0iMLG403QAGd5vZZhcgB2tg/iazewKeTff+HMRkusx+lWBYzBEGcRohY/FiA=="],
"@fluentui/react-dialog": ["@fluentui/react-dialog@9.16.3", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-aUnErTbSf2oqrqbQOCrjXp/12qHVfnxCR71/5hXJLME7BtYZ/m2lvs5r9MTjQSXBy8ar4G5jobS/+XJ0Lq3XqA=="],
"@fluentui/react-divider": ["@fluentui/react-divider@9.4.11", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-aESagOX6l7Ja9lb+3zJa6V5m1mjFnI4NEu8TccAu1VUlMZxX6flbMBJplgjN76dJjcHgs8uoa5xxxD74WNZBXg=="],
"@fluentui/react-drawer": ["@fluentui/react-drawer@9.10.9", "", { "dependencies": { "@fluentui/react-dialog": "^9.16.3", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-tlHZBkILOHnA7Lg2v/vzmOvTNrPYJnPJAqiceuFlUZWncIWWAUfpw4Teh5V0wGNr6/yC/HjUD5xnynvIhr/ZuA=="],
"@fluentui/react-field": ["@fluentui/react-field@9.4.11", "", { "dependencies": { "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-kF93G+LGEKaFJcEAUHJKZUc1xeV/q+JTygYVnEDkPbQ/4j+l+J3rVuHL8U7bhE+8cJG3wDP8jt4jqHsDgKyn5w=="],
"@fluentui/react-icons": ["@fluentui/react-icons@2.0.314", "", { "dependencies": { "@griffel/react": "^1.0.0", "tslib": "^2.1.0" }, "peerDependencies": { "react": ">=16.8.0 <20.0.0" } }, "sha512-oIfhvtrcQatubmJeUTQ6ME/ghlU0X8FY5eA8YwhgqtrvnPst5FOAIAfsQfFA30U7uPgcxg9psWYfQbtAc9DZhA=="],
"@fluentui/react-image": ["@fluentui/react-image@9.3.11", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-aLpz0/C6T0Uit6SmyhOJjYBvndZzfvmKv1vg+JRnE0aHS5jSUPoCLI6apxyMC6/LcqqTBklpqK3AD9kYpUxfqQ=="],
"@fluentui/react-infobutton": ["@fluentui/react-infobutton@9.0.0-beta.107", "", { "dependencies": { "@fluentui/react-icons": "^2.0.237", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-popover": "^9.12.12", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-BcI4e+Oj1B/Qk4CMd0O9H0YF+IL4nhK8xuzI5bsZ5mdCaXiwIBgy5RyP8HVSq3y+Ml4XD2IRwufplcxF2cgTOA=="],
"@fluentui/react-infolabel": ["@fluentui/react-infolabel@9.4.12", "", { "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-popover": "^9.12.12", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-inXlz5EAwQHKsGyB3wc5WmgQ1F9zc18x0HRd/otc2R7Oo1yRW5hXQCG5K5A9/wUge2pRiQVcBCsIurggmCNUhA=="],
"@fluentui/react-input": ["@fluentui/react-input@9.7.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-ae/5ttJf25+J8akeEXpXRFqUAePPt2Moyfx4Tj0u7ZgG1U9IFbcBsshKEHAmIaygueXf6KdRyOduh1CF6a/D2w=="],
"@fluentui/react-jsx-runtime": ["@fluentui/react-jsx-runtime@9.3.3", "", { "dependencies": { "@fluentui/react-utilities": "^9.25.4", "@swc/helpers": "^0.5.1", "react-is": "^17.0.2" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "react": ">=16.14.0 <20.0.0" } }, "sha512-KOy85JqR6MSmp7OKUk/IPleaRlUSWF247AM2Ksu9uEKzDBQ2MO3sYUt8X9457GZjIuKLn5J2Vk127W/Khe+/Bg=="],
"@fluentui/react-label": ["@fluentui/react-label@9.3.11", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-9LORj4JQJCbp2J5ftW7ZjDxzD3Y4BkszX3Y7L1mK8DPRVAKOuGiakbH7U0q7ClGOMhCinWIQJjKAwzPZLo7xgA=="],
"@fluentui/react-link": ["@fluentui/react-link@9.7.0", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-NQ5Jhe5WBYfANSmIcl6fE/oBeh7G4iAq1FU9L/hyva5dxQ9OtiOpU5wxqVFLKEID/r144rhdtOZPL5AcAuJKdg=="],
"@fluentui/react-list": ["@fluentui/react-list@9.6.6", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-checkbox": "^9.5.11", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-t0ret56WXP86rDfnhuRrWg/DuS2zZkomB/Nu444rVygE8hsjPUTm5DXx7JKy+sGKVLyFbtsbXNMICkbxhGSSRA=="],
"@fluentui/react-menu": ["@fluentui/react-menu@9.20.5", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-vshb/OXBZxvk+ghdmdVb2mJ/LJBYjlwpZRhWGJ8ZU0hmPTh74m5jTFWditSk8aL9oMvVuIo0MYLQyUJyJsFoqg=="],
"@fluentui/react-message-bar": ["@fluentui/react-message-bar@9.6.14", "", { "dependencies": { "@fluentui/react-button": "^9.6.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-link": "^9.7.0", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-UR4Uvkx4VHQyS04T5ikf9gYOH52dloo1vjmK+pFKiqRzZhflHEXID9R1AZFuuZ572KUMXnxRlyEevpXnWqE70w=="],
"@fluentui/react-motion": ["@fluentui/react-motion@9.11.4", "", { "dependencies": { "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-utilities": "^9.25.4", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-rLxz6DSAtp3O+W+mJnov2qXtvZkIgcC1BQOAyUH6tl6u2YmsC1/zRKWhVsf/WUgZwqu3G4jlq15ptyuCITAcDA=="],
"@fluentui/react-motion-components-preview": ["@fluentui/react-motion-components-preview@0.14.1", "", { "dependencies": { "@fluentui/react-motion": "*", "@fluentui/react-utilities": "*", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-+2MK7d2g3mD+6Z3o9/fitO+V4u5OKGeRUoUjwlU1v56JHP43hj+NCJynoe4Cym8FeSwTyipks6hvdqBF4W+jtw=="],
"@fluentui/react-nav": ["@fluentui/react-nav@9.3.14", "", { "dependencies": { "@fluentui/react-aria": "^9.17.6", "@fluentui/react-button": "^9.6.12", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-divider": "^9.4.11", "@fluentui/react-drawer": "^9.10.9", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-tooltip": "^9.8.11", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-0Lylul5g/9y3Cay5qHLtzW4SB9kdkTmvjHSffPJZDKE/Wv7GBbDypBxoB+f2L1K4f0qzRJ1NvIiwatH4hAsUDA=="],
"@fluentui/react-overflow": ["@fluentui/react-overflow@9.6.5", "", { "dependencies": { "@fluentui/priority-overflow": "^9.2.1", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-4MlXASDodkwk4QWhUPLgMbUPwDYAOAWDnQGJz4q6Hs9eZvx83dSpWdWjkmQ6mwjYf2HwooMkqsjR/kAFvg+ipg=="],
"@fluentui/react-persona": ["@fluentui/react-persona@9.5.12", "", { "dependencies": { "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-badge": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-ja3t1o6XDJWCJnOVDTM48G7bFPAbNxcsQKwAPfiuROVu8ODbTQefutCHl0Hno40AsftQk6N4zGbKcn7BYSZ09Q=="],
"@fluentui/react-popover": ["@fluentui/react-popover@9.12.12", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-IytuasB4b4lCnEhFC0OC66a3mzBSePLpg78/BceKYepuG7IC6iGuCwYartqSQCSUlSU12rT02/V0rqCO81f4Nw=="],
"@fluentui/react-portal": ["@fluentui/react-portal@9.8.8", "", { "dependencies": { "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-RVvhWYfcwIUYXiokgFw3oxb7Q6xox2e7jcsgFtheDm2X/BHT6WJigW4OaCjOkvugkBEYQkwgIpL9iS2QG3HMFA=="],
"@fluentui/react-positioning": ["@fluentui/react-positioning@9.20.10", "", { "dependencies": { "@floating-ui/devtools": "^0.2.3", "@floating-ui/dom": "^1.6.12", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-mjuiqh+urV5SzAP2NfzUzsvtWut0aNcO9m/jIuz374iTVGRfDNeVIl7aPI4yK5sdCDR6dGALiNMTFHpjz1YXyw=="],
"@fluentui/react-progress": ["@fluentui/react-progress@9.4.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-L0Yh2D0vLPJX0jYfc9VHf8c/idW+e/oRxYNXfrTrvtW1bX80bAmrXWgdRPr/VEtvbJh//2ol2TRmTTQsn2ECNQ=="],
"@fluentui/react-provider": ["@fluentui/react-provider@9.22.11", "", { "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/core": "^1.16.0", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-XrinA7DVEqsPHeN9XljwTENiIQypiF9cmDYXHN9Emsz6Od4hnmsbt4pnR4Xsf+GcSxVtxkIImfgwtS0aENzYbQ=="],
"@fluentui/react-radio": ["@fluentui/react-radio@9.5.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-tMxCcqRSSYqYr6hy1dKkzS6LymRc8wM089vr4eBLPQCGCvi3OCd6P7XH8aIcXnzxE3+v03Gs7E/wbzi2CXN6gA=="],
"@fluentui/react-rating": ["@fluentui/react-rating@9.3.11", "", { "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-9Bl/sESNbFTbz8peGt9vxLxHDO0AWvS12oMiQ80S1GQOt1ua4S9/SKC83OvyVLEdpBDpBkXTelNz5whczcWexQ=="],
"@fluentui/react-search": ["@fluentui/react-search@9.3.11", "", { "dependencies": { "@fluentui/react-icons": "^2.0.245", "@fluentui/react-input": "^9.7.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-inLoPgbGnupfwhBxFS59mF/ThsntusfYp9TaaTB3SJmqfEEx6YXi5soxszzrXsNvrqpgEoCGIduRpEICuUz5pw=="],
"@fluentui/react-select": ["@fluentui/react-select@9.4.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-/mcdl/lkKccT+GKXu22y2/ANeLhFNUdjkOX+0rvBdl3u49xkqS9Y4Bi0zM1EhhTV2jE8+yjMjzPDzfzJaXVK1A=="],
"@fluentui/react-shared-contexts": ["@fluentui/react-shared-contexts@9.26.0", "", { "dependencies": { "@fluentui/react-theme": "^9.2.0", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "react": ">=16.14.0 <20.0.0" } }, "sha512-r52B+LUevs930pe45pFsppM9XNvY+ojgRgnDE+T/6aiwR/Mo4YoGrtjhLEzlQBeTGuySICTeaAiXfuH6Keo5Dg=="],
"@fluentui/react-skeleton": ["@fluentui/react-skeleton@9.4.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-nw6NlTBXS7lNSxsebLuADYQi9gJ83jFBFsFq+AGIpAoZLBOCHOhk8/XwV3vYtPwVrKcZtOtXqh9NdCqTR3OAIA=="],
"@fluentui/react-slider": ["@fluentui/react-slider@9.5.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-kxZKklJbcG/521muQaIDMdcftoClbwV7yMOcu8PMG+VXsaIuoandoBleBYdzM2XdpY62iK6vUPAMZWBZh3B5Ng=="],
"@fluentui/react-spinbutton": ["@fluentui/react-spinbutton@9.5.11", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-pYR3RkJfks+0WV47KoDKD04D0pTHtT+lu3AeOpBlIswxtsb1gZEDmTrEHHNeLDKKVhWMWNoEPlxfXuX9tOh7yA=="],
"@fluentui/react-spinner": ["@fluentui/react-spinner@9.7.11", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-MhmAisICa3BzBNQH9CnLI5NVPTTXFo1Yaey8kbQPU+gVVF4vIGORB7M1MXSHFxZvojtFpBixiVHqRwh9mowJww=="],
"@fluentui/react-swatch-picker": ["@fluentui/react-swatch-picker@9.4.11", "", { "dependencies": { "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-M/ZfHqo63F69y2ymEQDDN/BZuI3afeW3U+omyGZZoHts3rVCjPk6sKFemTRpUhGgGfxBdexWEPithZx3dk0IPA=="],
"@fluentui/react-switch": ["@fluentui/react-switch@9.4.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-label": "^9.3.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-/WDcoVFQ3I2fe5FTINfyVTIW6wuTgM5QkJgcwbU7HTANq/+wJ2f8wzywoI4x16cJOckBdy+ByDpW7uJ/Uvs8RA=="],
"@fluentui/react-table": ["@fluentui/react-table@9.19.5", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-checkbox": "^9.5.11", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-radio": "^9.5.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-In9egEdytjFd6N1RBZd5+3UgdXvEVDP7rz+/I79J10ui2+Nb7r9ah68m5CQB15AKA8F5XFDWPEGvGG3Tmuq4Jg=="],
"@fluentui/react-tabs": ["@fluentui/react-tabs@9.10.7", "", { "dependencies": { "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-Kfq6GxZXEKsMdGKmHWNMcEYOYHxl5+fXJOH6ZRgeR2FkHUsPUUe2BHaFnOMRSvCwzECvhOMYs+Ekqt7JzW3BWQ=="],
"@fluentui/react-tabster": ["@fluentui/react-tabster@9.26.10", "", { "dependencies": { "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "keyborg": "^2.6.0", "tabster": "^8.5.5" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-KrddtwbnbgYVAnOkx1pQsMMgq7Kfi+lMRrUrDDJ9Y5X6wiXiajbWRRxYgKiOJc3MpeDCaTCEtjOWNG92vcinMw=="],
"@fluentui/react-tag-picker": ["@fluentui/react-tag-picker@9.7.12", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-combobox": "^9.16.12", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-field": "^9.4.11", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-tags": "^9.7.12", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-OJucCDub6b3ceGL6v2UXL+SD3x6nJMbmJ70v38BmrA9t3fNcDvn6RnsfHhF2O0pRGGUOrXbK7vDwVhUAG4Py8w=="],
"@fluentui/react-tags": ["@fluentui/react-tags@9.7.12", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-G7pxP0GGa6J/7mYvB9ycOmD9Jpm6ByUz6JsJI4OBL9UnhenUVTtE7ZKJ9GJ0SiG0GVxS152aSlOR7NLHV7mCqw=="],
"@fluentui/react-teaching-popover": ["@fluentui/react-teaching-popover@9.6.12", "", { "dependencies": { "@fluentui/react-aria": "^9.17.6", "@fluentui/react-button": "^9.6.12", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-popover": "^9.12.12", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1", "use-sync-external-store": "^1.2.0" }, "peerDependencies": { "@types/react": ">=16.8.0 <20.0.0", "@types/react-dom": ">=16.8.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.8.0 <20.0.0" } }, "sha512-Ugo5SQ3yzSlxUWkeeEdumTWTw662KDh3UPc6RGhU0Jq13skpmsClSJL678BZwsYdAaJXvvG9Bi4PjPeezeB/SA=="],
"@fluentui/react-text": ["@fluentui/react-text@9.6.11", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-U7EiCesOWjkALf7LM6sy+yvE59Px3c6f27jg4aa21UMo61HCVNbjKV8Lz6GzEftEvv++/EZ25yZBiQcKgh/5iA=="],
"@fluentui/react-textarea": ["@fluentui/react-textarea@9.6.11", "", { "dependencies": { "@fluentui/react-field": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-5ds8u8hzSqj8cOy0e7HJWjUMq1aO0MIJiaNt/SyIxoZFvsklj/2yaMRVXpWxr3GvX5bzScvFoBY53gPdLKtE/g=="],
"@fluentui/react-theme": ["@fluentui/react-theme@9.2.0", "", { "dependencies": { "@fluentui/tokens": "1.0.0-alpha.22", "@swc/helpers": "^0.5.1" } }, "sha512-Q0zp/MY1m5RjlkcwMcjn/PQRT2T+q3bgxuxWbhgaD07V+tLzBhGROvuqbsdg4YWF/IK21zPfLhmGyifhEu0DnQ=="],
"@fluentui/react-toast": ["@fluentui/react-toast@9.7.9", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-PaFh2CwVK4tgvRzBMb46ODHsB+ZYSYE8mx735vqgIG8Oj1AL3wZ5Y9TrjJGxn/lppZgtnwLgt4GQ+GI7MM+e+g=="],
"@fluentui/react-toolbar": ["@fluentui/react-toolbar@9.6.12", "", { "dependencies": { "@fluentui/react-button": "^9.6.12", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-divider": "^9.4.11", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-radio": "^9.5.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-AuOZvp6Jcc/Sngk0OddTsHlJVU/u9mVEw6JDhsCYiwKeq04kdgfco1sjSTGjDhJbf1SnkhmyR6YN16SrpVQWtA=="],
"@fluentui/react-tooltip": ["@fluentui/react-tooltip@9.8.11", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-portal": "^9.8.8", "@fluentui/react-positioning": "^9.20.10", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-ke7Hbom3dtC3f9QjJG/F7QfNfukwTtAhoYLmwwQnXYTh/CIVxoC2rVh4c/V8jUD0lnjNPBZZ5ttVUopWljHuFg=="],
"@fluentui/react-tree": ["@fluentui/react-tree@9.15.6", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-aria": "^9.17.6", "@fluentui/react-avatar": "^9.9.12", "@fluentui/react-button": "^9.6.12", "@fluentui/react-checkbox": "^9.5.11", "@fluentui/react-context-selector": "^9.2.12", "@fluentui/react-icons": "^2.0.245", "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-motion": "^9.11.4", "@fluentui/react-motion-components-preview": "^0.14.1", "@fluentui/react-radio": "^9.5.11", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-tabster": "^9.26.10", "@fluentui/react-theme": "^9.2.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-L/uc+SgwXW8DXgSZsyIg5tQkixfrGllANg0I2578WRlfOkERehkg1eSW8Uib/Mbk+W3tB0I8CL20ifoSTL7Ztw=="],
"@fluentui/react-utilities": ["@fluentui/react-utilities@9.25.4", "", { "dependencies": { "@fluentui/keyboard-keys": "^9.0.8", "@fluentui/react-shared-contexts": "^9.26.0", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "react": ">=16.14.0 <20.0.0" } }, "sha512-vvEIFTfqkcBnKNJhlm8csdGNtOWDWDkqAM4tGlW7jLlFrhNkOfDsqdNuBElENPNJ1foHyVTF5ZSr20kVoKWPjQ=="],
"@fluentui/react-virtualizer": ["@fluentui/react-virtualizer@9.0.0-alpha.107", "", { "dependencies": { "@fluentui/react-jsx-runtime": "^9.3.3", "@fluentui/react-shared-contexts": "^9.26.0", "@fluentui/react-utilities": "^9.25.4", "@griffel/react": "^1.5.32", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", "react": ">=16.14.0 <20.0.0", "react-dom": ">=16.14.0 <20.0.0" } }, "sha512-zpTVzJB2BUNv7QdTUlLSBMCbt/EfALRuls/u/8FYaO4PGOFVeS3equytyxSOizz9zJZVhm8sjdp326DEQNiaPA=="],
"@fluentui/tokens": ["@fluentui/tokens@1.0.0-alpha.22", "", { "dependencies": { "@swc/helpers": "^0.5.1" } }, "sha512-i9fgYyyCWFRdUi+vQwnV6hp7wpLGK4p09B+O/f2u71GBXzPuniubPYvrIJYtl444DD6shLjYToJhQ1S6XTFwLg=="],
"@griffel/core": ["@griffel/core@1.19.2", "", { "dependencies": { "@emotion/hash": "^0.9.0", "@griffel/style-types": "^1.3.0", "csstype": "^3.1.3", "rtl-css-js": "^1.16.1", "stylis": "^4.2.0", "tslib": "^2.1.0" } }, "sha512-WkB/QQkjy9dE4vrNYGhQvRRUHFkYVOuaznVOMNTDT4pS9aTJ9XPrMTXXlkpcwaf0D3vNKoerj4zAwnU2lBzbOg=="],
"@griffel/react": ["@griffel/react@1.5.32", "", { "dependencies": { "@griffel/core": "^1.19.2", "tslib": "^2.1.0" }, "peerDependencies": { "react": ">=16.8.0 <20.0.0" } }, "sha512-jN3SmSwAUcWFUQuQ9jlhqZ5ELtKY21foaUR0q1mJtiAeSErVgjkpKJyMLRYpvaFGWrDql0Uz23nXUogXbsS2wQ=="],
"@griffel/style-types": ["@griffel/style-types@1.3.0", "", { "dependencies": { "csstype": "^3.1.3" } }, "sha512-bHwD3sUE84Xwv4dH011gOKe1jul77M1S6ZFN9Tnq8pvZ48UMdY//vtES6fv7GRS5wXYT4iqxQPBluAiYAfkpmw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="],
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
"@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="],
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
"csstype": ["csstype@3.2.0", "", {}, "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg=="],
"embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="],
"embla-carousel-autoplay": ["embla-carousel-autoplay@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA=="],
"embla-carousel-fade": ["embla-carousel-fade@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg=="],
"hono": ["hono@4.10.6", "", {}, "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"keyborg": ["keyborg@2.6.0", "", {}, "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"pocketbase": ["pocketbase@0.26.3", "", {}, "sha512-5deUKRoEczpxxuHzwr6/DHVmgbggxylEVig8CKN+MjvtYxPUqX/C6puU0yaR2yhTi8zrh7J9s7Ty+qBGwVzWOQ=="],
"react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
"react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
"react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"rtl-css-js": ["rtl-css-js@1.16.1", "", { "dependencies": { "@babel/runtime": "^7.1.2" } }, "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg=="],
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
"tabster": ["tabster@8.5.6", "", { "dependencies": { "keyborg": "2.6.0", "tslib": "^2.8.1" }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.40.0" } }, "sha512-2vfrRGrx8O9BjdrtSlVA5fvpmbq5HQBRN13XFRg6LAvZ1Fr3QdBnswgT4YgFS5Bhoo5nxwgjRaRueI2Us/dv7g=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
}
}
+3
View File
@@ -0,0 +1,3 @@
[dev]
port = 3000
static = "./public"
+4
View File
@@ -0,0 +1,4 @@
11/19/2025 Server running on http://localhost:3000 Uses Bun and Hono latest versions
11/19/2025 Adding Oauth capabilities for authentication and security and to use Microsoft Graph API for future integration as part of this @azure/msal-browser is installed
+1
View File
@@ -0,0 +1 @@
console.log("Hello via Bun!");
+24
View File
@@ -0,0 +1,24 @@
{
"name": "job-info",
"type": "module",
"private": true,
"scripts": {
"dev": "bun src/server.ts",
"start": "bun run src/server.ts",
"server": "bun run src/server.ts"
},
"dependencies": {
"@azure/msal-browser": "^4.26.1",
"@fluentui/react-components": "9.72.7",
"hono": "^4.10.4",
"pocketbase": "^0.26.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.6.3"
}
}
View File
View File
@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Rich Text Editor - Free for Business</title>
<!-- Quill CSS -->
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet" />
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f4f6f9;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}
header {
background: #1a73e8;
color: white;
padding: 1rem;
text-align: center;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
.editor-wrapper {
padding: 20px;
}
#toolbar {
border: none;
border-bottom: 1px solid #ddd;
padding: 10px;
background: #f9f9f9;
}
#editor {
height: 400px;
font-size: 16px;
line-height: 1.6;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 16px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #1557b0;
}
button.secondary {
background: #5f6368;
}
button.secondary:hover {
background: #484c50;
}
#output {
margin-top: 30px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
white-space: pre-wrap;
font-family: monospace;
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Rich Text Editor (Quill.js - MIT License)</h1>
</header>
<div class="editor-wrapper">
<!-- Toolbar will be injected here by Quill -->
<div id="toolbar"></div>
<!-- Editor -->
<div id="editor">
<p>Start typing your content here...</p>
<p>Use the toolbar to <strong>bold</strong>, <em>italicize</em>, add <u>underline</u>, or insert lists and links.</p>
</div>
<!-- Custom Controls -->
<div class="controls">
<button id="save-html">Save as HTML</button>
<button id="get-json">Get JSON</button>
<button class="secondary" id="clear">Clear Editor</button>
<button class="secondary" id="load-sample">Load Sample</button>
</div>
<!-- Output Display -->
<pre id="output"></pre>
</div>
</div>
<!-- Quill JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
<script>
// Initialize Quill editor
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'align': [] }],
['clean']
]
},
placeholder: 'Compose an epic...'
});
// Custom Buttons
document.getElementById('save-html').addEventListener('click', () => {
const html = quill.root.innerHTML;
const output = document.getElementById('output');
output.textContent = html;
output.style.display = 'block';
alert('HTML copied to output below!');
});
document.getElementById('get-json').addEventListener('click', () => {
const delta = quill.getContents();
const output = document.getElementById('output');
output.textContent = JSON.stringify(delta, null, 2);
output.style.display = 'block';
alert('Delta JSON copied to output!');
});
document.getElementById('clear').addEventListener('click', () => {
if (confirm('Clear all content?')) {
quill.setContents([]);
}
});
document.getElementById('load-sample').addEventListener('click', () => {
quill.setContents({
ops: [
{ insert: 'Welcome to Your Rich Text Editor!\n', attributes: { header: 1 } },
{ insert: 'This is ' },
{ insert: 'bold', attributes: { bold: true } },
{ insert: ', this is ' },
{ insert: 'italic', attributes: { italic: true } },
{ insert: ', and this is a ' },
{ insert: 'link', attributes: { link: 'https://quilljs.com' } },
{ insert: '.\n\n' },
{ insert: 'Features include:\n', attributes: { list: 'bullet' } },
{ insert: 'Images & embeds\n', attributes: { list: 'bullet' } },
{ insert: 'Custom styles & output\n', attributes: { list: 'bullet' } },
{ insert: '100% free for business use\n', attributes: { list: 'bullet' } }
]
});
});
/*
* Rich Text Editor powered by Quill.js v2.0.2
* © 20152025 Slab, Inc. and contributors
* MIT License https://quilljs.com
* Thank you to the Quill team for this excellent open-source editor.
*/
</script>
</body>
</html>
@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Rich Text Editor - Free for Business</title>
<!-- Quill CSS -->
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet" />
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f4f6f9;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
overflow: hidden;
}
header {
background: #1a73e8;
color: white;
padding: 1rem;
text-align: center;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
.editor-wrapper {
padding: 20px;
}
#toolbar {
border: none;
border-bottom: 1px solid #ddd;
padding: 10px;
background: #f9f9f9;
}
#editor {
height: 400px;
font-size: 16px;
line-height: 1.6;
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 10px 16px;
background: #1a73e8;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #1557b0;
}
button.secondary {
background: #5f6368;
}
button.secondary:hover {
background: #484c50;
}
#output {
margin-top: 30px;
padding: 15px;
background: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 8px;
white-space: pre-wrap;
font-family: monospace;
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Rich Text Editor (Quill.js - MIT License)</h1>
</header>
<div class="editor-wrapper">
<!-- Toolbar will be injected here by Quill -->
<div id="toolbar"></div>
<!-- Editor -->
<div id="editor">
<p>Start typing your content here...</p>
<p>Use the toolbar to <strong>bold</strong>, <em>italicize</em>, add <u>underline</u>, or insert lists and links.</p>
</div>
<!-- Custom Controls -->
<div class="controls">
<button id="save-html">Save as HTML</button>
<button id="get-json">Get JSON</button>
<button class="secondary" id="clear">Clear Editor</button>
<button class="secondary" id="load-sample">Load Sample</button>
</div>
<!-- Output Display -->
<pre id="output"></pre>
</div>
</div>
<!-- Quill JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
<script>
// Initialize Quill editor
const quill = new Quill('#editor', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'align': [] }],
['clean']
]
},
placeholder: 'Compose an epic...'
});
// Custom Buttons
document.getElementById('save-html').addEventListener('click', () => {
const html = quill.root.innerHTML;
const output = document.getElementById('output');
output.textContent = html;
output.style.display = 'block';
alert('HTML copied to output below!');
});
document.getElementById('get-json').addEventListener('click', () => {
const delta = quill.getContents();
const output = document.getElementById('output');
output.textContent = JSON.stringify(delta, null, 2);
output.style.display = 'block';
alert('Delta JSON copied to output!');
});
document.getElementById('clear').addEventListener('click', () => {
if (confirm('Clear all content?')) {
quill.setContents([]);
}
});
document.getElementById('load-sample').addEventListener('click', () => {
quill.setContents({
ops: [
{ insert: 'Welcome to Your Rich Text Editor!\n', attributes: { header: 1 } },
{ insert: 'This is ' },
{ insert: 'bold', attributes: { bold: true } },
{ insert: ', this is ' },
{ insert: 'italic', attributes: { italic: true } },
{ insert: ', and this is a ' },
{ insert: 'link', attributes: { link: 'https://quilljs.com' } },
{ insert: '.\n\n' },
{ insert: 'Features include:\n', attributes: { list: 'bullet' } },
{ insert: 'Images & embeds\n', attributes: { list: 'bullet' } },
{ insert: 'Custom styles & output\n', attributes: { list: 'bullet' } },
{ insert: '100% free for business use\n', attributes: { list: 'bullet' } }
]
});
});
/*
* Rich Text Editor powered by Quill.js v2.0.2
* © 20152025 Slab, Inc. and contributors
* MIT License https://quilljs.com
* Thank you to the Quill team for this excellent open-source editor.
*/
</script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job List</title>
<!-- AG-Grid CSS (CDN) -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ag-grid-community@32.0.0/styles/ag-grid.css">
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/ag-grid-community@32.0.0/styles/ag-theme-alpine.css">
<style>
body {font-family:Arial,sans-serif;margin:20px;background:#f8f9fa;}
.searchContainer{max-width:1600px;}
.searchBox{background-color:#ccc;width:15%;padding:10px;border-radius:8px;border:1px solid#d1d5db;box-sizing:border-box;font-size: 18px;}
.searchBoxInput {width:100%;outline:none;background:transparent;font-size:inherit;border:none;}
.searchBoxInput::placeholder {color:#9ca3af;}
.container {max-width:1600px;margin:auto;background:#fff;padding:20px;
border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,.1);}
#myGrid {height:750px;width:100%;}
</style>
</head>
<body>
<div class="searchContainer">
<div class="searchBox"style="text-align: right;">
<input id="searchBoxInput" placeholder="Search jobs (just start typing)">
</div>
<div class="container">
<div id="myGrid" class="ag-theme-alpine"></div>
</div>
</div>
<!-- AG-Grid JS (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/ag-grid-community@32.0.0/dist/ag-grid-community.min.js"></script>
<script>
/* ---------- 100 rows of sample data ---------- */
const rowData = Array.from({length:100},(_,i)=>({
job_number: 1000+i,
job_name: `Project ${String.fromCharCode(65+(i%26))}${i+1}`,
city: `City ${i+1}`,
country: 'USA',
email: `job${i+1}@corp.com`,
phone: `555-${String(100+i).padStart(3,'0')}`,
dept: ['Eng','Mkt','Sales','HR','Fin','IT'][i%6],
salary: `$${(90+i%20)*1000}`,
join: `2024-${String((i%12)+1).padStart(2,'0')}-01`,
status: i%8===0?'Review':'Active',
projA: `${65+i%35}%`, projB: `${70+i%30}%`, projC: `${73+i%27}%`,
rating: (4.0+(i%10)/10).toFixed(1),
notes: 'On track',
manager: `M${i%5+1}`,
team: `T${String.fromCharCode(65+i%8)}`,
region: ['N','S','E','W'][i%4],
bonus: `$${i*400}`,
review: 'Good'
}));
/* ---------- Column definitions ---------- */
const columnDefs = [
{field:'job_number',headerName:'Job_Number',pinned:'left',width:120,hide:false},
{field:'job_name', headerName:'Job_Name', pinned:'left',width:160},
{field:'city', headerName:'City'},
{field:'country', headerName:'Country'},
{field:'email', headerName:'Email'},
{field:'phone', headerName:'Phone'},
{field:'dept', headerName:'Dept'},
{field:'salary', headerName:'Salary'},
{field:'join', headerName:'Join'},
{field:'status', headerName:'Status'},
{field:'projA', headerName:'Proj A'},
{field:'projB', headerName:'Proj B'},
{field:'projC', headerName:'Proj C'},
{field:'rating', headerName:'Rating'},
{field:'notes', headerName:'Notes'},
{field:'manager', headerName:'Manager'},
{field:'team', headerName:'Team'},
{field:'region', headerName:'Region'},
{field:'bonus', headerName:'Bonus'},
{field:'review', headerName:'Review'}
];
/* ---------- Grid options ---------- */
const gridOptions = {
columnDefs,
rowData,
defaultColDef:{resizable:true,sortable:true,filter:true},
onGridReady: params => {
window.gridApi = params.api;
window.columnApi = params.columnApi;
}
};
/* ---------- Create the grid ---------- */
new agGrid.Grid(document.querySelector('#myGrid'), gridOptions);
</script>
</body>
</html>
View File
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {font-family:system-ui;max-width:600px;margin:40px auto;padding:20px;}
input, button {width:100%;padding:12px;margin:8px 0;font-size:16px;border:1px solid #ccc;border-radius:6px;}
button {background:#0d6efd;color:#fff;border:none;cursor:pointer;font-weight:bold;}
button:disabled {background:#6c757d;cursor:not-allowed;}
.out {margin-top:16px;padding:12px;border-radius:6px;font-weight:bold;}
.success {background:#d1e7dd;color:#0f5132;}
.error {background:#f8d7da;color:#842029;}
</style>
<title>PB Export - Component Ready</title>
</head>
<body>
<input type="email" id="email" placeholder="admin@example.com">
<input type="password" id="pass" placeholder="password">
<input type="text" id="token" readonly style="background:#f4f4f4;">
<button id="login">Log In</button>
<button id="exportB" disabled>Export Job_Info CSV</button>
<div id="status"></div>
<script type="module">
import PocketBase from "https://cdn.jsdelivr.net/npm/pocketbase@latest/dist/pocketbase.es.mjs";
document.addEventListener("DOMContentLoaded", () => {
// --- Initialize PocketBase ---
const pb = new PocketBase('http://10.10.1.109:8080');
pb.authStore.clear();
// --- Select DOM elements ---
const email = document.getElementById('email');
const pass = document.getElementById('pass');
const token = document.getElementById('token');
const loginB = document.getElementById('login');
const exportB = document.getElementById('exportB');
const status = document.getElementById('status');
// --- Helper functions ---
function showSuccess(msg) {
status.className = 'out success';
status.textContent = msg;
}
function showError(msg) {
status.className = 'out error';
status.textContent = msg;
}
function downloadCSV(content, filename) {
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
}
// --- Login handler ---
async function getToken() {
try {
loginB.disabled = true;
exportB.disabled = true;
if (!email.value || !pass.value) throw new Error("Email and password are required.");
await pb.collection('_superusers').authWithPassword(email.value, pass.value);
if (!pb.authStore.isValid) throw new Error("Login failed.");
token.value = pb.authStore.token;
showSuccess("Logged in successfully.");
exportB.disabled = false; // enable export after successful login
} catch(err) {
showError("Login failed: " + err.message);
} finally {
loginB.disabled = false;
}
}
loginB.addEventListener("click", getToken);
// --- Export handler ---
exportB.onclick = async () => {
try {
exportB.disabled = true;
const records = await pb.collection('Job_Info').getFullList({ sort: 'Job_Number' });
if (!records.length) throw new Error('No records found.');
// --- Generate CSV ---
const sample = records[0];
const fields = ['pb_id', ...Object.keys(sample).filter(f => f !== 'id')];
const csv = [
fields.join(','),
...records.map(r => {
const row = { pb_id: r.id }; // use record ID
fields.slice(1).forEach(f => row[f] = r[f] ?? '');
return fields.map(f => `"${String(row[f]).replace(/"/g, '""')}"`).join(',');
})
].join('\n');
downloadCSV(csv, `Job_Info_Export_${new Date().toISOString().slice(0,10)}.csv`);
showSuccess(`Exported ${records.length} records successfully.`);
} catch(err) {
showError("Export failed: " + err.message);
} finally {
exportB.disabled = false;
}
};
});
</script>
</body>
</html>
@@ -0,0 +1,467 @@
<!--Notes-->
<!--11/26/2025 Updated Sync Section to avoid duplicate creation and update existing records even if pb_id isn't present
// Updated header map-->
<!--Prod Ready as of 11/17/2025-->
<!--Use Google Chrome-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Sheet → PocketBase Sync (Final Forever Version)</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 560px;
margin: 30px auto;
padding: 20px;
background: #f4f6f9;
color: #1a1a1a;
line-height: 1.5;
}
h1 { font-size: 1.6rem; color: #0d6efd; margin-bottom: 8px; }
p { margin: 8px 0; font-size: 0.95rem; color: #444; }
input, button {
width: 100%;
padding: 14px;
margin: 10px 0;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 8px;
box-sizing: border-box;
}
button {
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
/* Button States */
#loginBtn { background: #0d6efd; color: white; }
#loginBtn.logged-in { background: #198754; } /* Green = logged in */
#syncBtn { background: #6c757d; }
#syncBtn.ready { background: #198754; } /* Green = ready to sync */
#syncBtn.syncing { background: #0d6efd; }
#syncBtn.done { background: #198754; } /* Same green as login */
#deleteAllBtn { background: #dc3545; }
.file-drop {
border: 3px dashed #0d6efd;
padding: 50px;
text-align: center;
border-radius: 12px;
background: #f0f8ff;
font-size: 18px;
color: #0d6efd;
cursor: pointer;
transition: all 0.3s;
}
.file-drop.dragover { background: #cce5ff; border-color: #0069d9; }
.progress { margin: 12px 0; height: 8px; background: #e0e0e0; border-radius: 4px; overflow: hidden; }
.progress-bar { height: 100%; background: #0d6efd; width: 0%; transition: width 0.4s; }
.out { padding: 14px; border-radius: 8px; font-weight: bold; margin-top: 16px; }
.success { background: #d1e7dd; color: #0f5132; }
.error { background: #f8d7da; color: #842029; }
.info {
background: #0a0a0a;
color: #0ff;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
max-height: 500px;
overflow: auto;
margin-top: 16px;
white-space: pre-wrap;
}
.section {
background: white;
padding: 16px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin: 16px 0;
}
.note {
font-size: 0.85rem;
color: #666;
margin-top: 20px;
padding: 12px;
background: #fffbe6;
border-left: 4px solid #ffe58f;
border-radius: 0 6px 6px 0;
}
</style>
</head>
<body>
<div class="section">
<h1>Job Sheet → PocketBase Sync</h1>
<p>Headers = Excel Row 3 • Data starts = Row 4 • Zero violations • Fully documented</p>
</div>
<form autocomplete="on">
<input type="text" id="pbUrl" placeholder="PocketBase URL" value="http://10.10.1.109:8080" autocomplete="url">
<input type="email" id="email" placeholder="Admin email" autocomplete="username email">
<input type="password" id="pass" placeholder="Password" autocomplete="current-password">
<div class="file-drop" id="dropZone">
Drag & drop your Job Sheet here<br>or click to select
<input type="file" id="excelFile" accept=".xlsx,.xls" style="display:none;">
</div>
<div class="progress"><div class="progress-bar" id="progressBar"></div></div>
<button type="button" id="loginBtn">1. Login to PocketBase</button>
<button type="button" id="syncBtn" disabled>2. SYNC to Job_Info</button>
<button type="button" id="deleteAllBtn" disabled>DELETE ALL RECORDS (DANGER)</button>
</form>
<div id="status" class="out"></div>
<pre id="debug" class="info"></pre>
<div class="note">
<strong>Future-Proof Notes:</strong><br>
• If the Excel layout ever changes (e.g. headers move to Row 4), change only the two numbers in the "ROW CONFIG" section.<br>
• If PocketBase field names change, update only the HEADER_MAP below.<br>
• If CDN links break in 5+ years, replace the two &lt;script&gt; imports with newer ones from cdn.jsdelivr.net.
</div>
<script type="module">
// ================================================
// 1. IMPORTS — These load the libraries from the internet
// If they ever break, go to https://cdn.jsdelivr.net and get new links
// ================================================
import PocketBase from "https://cdn.jsdelivr.net/npm/pocketbase@latest/dist/pocketbase.es.mjs";
import * as XLSX from "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/xlsx.mjs";
// ================================================
// 2. DOM ELEMENTS — Getting buttons and inputs
// ================================================
const $ = id => document.getElementById(id);
const pbUrl = $('pbUrl');
const emailInput = $('email');
const pass = $('pass');
const fileInput = $('excelFile');
const dropZone = $('dropZone');
const loginBtn = $('loginBtn');
const syncBtn = $('syncBtn');
const deleteAllBtn = $('deleteAllBtn');
const status = $('status');
const debug = $('debug');
const progressBar = $('progressBar');
let pb = null; // Will hold PocketBase connection
let tableData = []; // Will hold parsed Excel rows
const COLLECTION = 'Job_Info';
// ================================================
// 3. ROW CONFIG — Only change these if Excel layout changes!
// Your current file:
// Row 1 (index 0) → big title
// Row 2 (index 1) → group labels
// Row 3 (index 2) → REAL headers ← we use this
// Row 4 (index 3) → first real data
// ================================================
const HEADER_ROW_INDEX = 2; // ← Row 3 in Excel
const DATA_START_ROW_INDEX = 3; // ← Row 4 in Excel
// ================================================
// 4. HEADER MAPPING — Excel column name → PocketBase field name
// Only edit the right side if your PocketBase fields change
// ================================================
const HEADER_MAP = {
"Job_Number": "Job_Number",
"Job_Full_Name": "Job_Full_Name",
"Job_Name": "Job_Name",
"Job Address": "Job_Address",
"Job Type": "Job_Type",
"Flag": "Flag",
"Job Status": "Job_Status",
"Job Division": "Job_Division",
"Estimator": "Estimator",
"Office Rep": "Office_Rep",
"Manager": "Manager",
"Company/Client": "Company_Client",
"Contact Person": "Contact_Person",
"Phone Number": "Phone_Number",
"Email": "Email",
"Due Date": "Due_Date",
"Due Date Source": "Due_Date_Source",
"Due Date Counter": "Due_Date_Counter",
"Due Time": "Due_Time",
"Docs Uploaded": "Docs_Uploaded",
"QB Created": "QB_Created",
"Added to Calendar": "Added to Calendar",
"PSwift Uploaded": "PSwift_Uploaded",
"Voxer Created": "Voxer_Created",
"Estimate Reviewed": "Estimate_Reviewed",
"Estimate Draft": "Estimate_Draft",
"Estimate Approved": "Estimate_Approved",
"Start Date": "Start_Date",
"Schedule\nConfidence": "Schedule_Confidence", // handles newline in header
"Notes": "Notes",
"Job QB Link": "Job_QB_Link",
"Voxer Link": "Voxer_Link",
"Job_Codes": "Job_Codes",
"Submission Date": "Submission_Date",
"Active": "Active",
"Job Folder Link": "Job_Folder_Link",
"Tax Exempt": "Tax_Exempt",
"pb_id": "id"
};
// ================================================
// 5. DRAG & DROP + FILE SELECTION
// ================================================
dropZone.addEventListener('click', () => fileInput.click());
['dragenter','dragover','dragleave','drop'].forEach(ev =>
dropZone.addEventListener(ev, e => { e.preventDefault(); e.stopPropagation(); })
);
dropZone.addEventListener('dragover', () => dropZone.classList.add('dragover'));
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', e => {
dropZone.classList.remove('dragover');
if (e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', () => {
if (fileInput.files[0]) loadFile(fileInput.files[0]);
});
// ================================================
// 6. EXCEL PARSING — With progress + no freezing
// Uses async yielding to avoid "long task" warnings
// ================================================
function loadFile(file) {
debug.textContent = `File: ${file.name} (${(file.size/1024/1024).toFixed(2)} MB)\nReading...\n`;
progressBar.style.width = '0%';
const reader = new FileReader();
reader.onload = async e => {
const data = e.target.result;
await parseExcelWithYield(data);
};
reader.readAsArrayBuffer(file);
}
async function parseExcelWithYield(data) {
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const range = XLSX.utils.decode_range(sheet['!ref']);
const merges = sheet['!merges'] || [];
const getValue = (r, c) => {
const addr = XLSX.utils.encode_cell({ r, c });
const cell = sheet[addr];
if (cell) return cell.v ?? cell.w ?? null;
for (const m of merges) {
if (m.s.r <= r && r <= m.e.r && m.s.c <= c && c <= m.e.c) {
const master = sheet[XLSX.utils.encode_cell({ r: m.s.r, c: m.s.c })];
return master ? (master.v ?? master.w ?? null) : null;
}
}
return null;
};
// Read headers
const rawHeaders = [];
for (let c = range.s.c; c <= range.e.c; c++) {
let h = getValue(HEADER_ROW_INDEX, c);
rawHeaders.push(h ? String(h).trim() : '');
}
debug.textContent += `\nFound ${rawHeaders.length} columns in Row 3\n`;
debug.textContent += `Headers: ${rawHeaders.slice(0, 20).join(' | ')}${rawHeaders.length > 20 ? ' ...' : ''}\n\n`;
// Parse data rows
const total = range.e.r - DATA_START_ROW_INDEX + 1;
let processed = 0;
const rows = [];
for (let r = DATA_START_ROW_INDEX; r <= range.e.r; r++) {
const row = {};
let hasData = false;
for (let c = range.s.c; c <= range.e.c; c++) {
const excelHeader = rawHeaders[c - range.s.c];
const pbField = HEADER_MAP[excelHeader];
if (!pbField) continue;
let val = getValue(r, c);
if (val !== null && val !== undefined) {
const s = String(val).trim();
if (s === '' || /^n\/?a$/i.test(s) || s === '#N/A') val = null;
}
row[pbField] = val;
if (val !== null && val !== '') hasData = true;
}
if (hasData) rows.push(row);
processed++;
// Show progress + yield control every 40 rows → no violations
if (processed % 40 === 0) {
progressBar.style.width = (processed / total * 100) + '%';
await new Promise(res => setTimeout(res, 0));
}
}
tableData = rows;
progressBar.style.width = '100%';
setTimeout(() => progressBar.style.width = '0%', 1200);
showSuccess(`Loaded ${rows.length} real jobs Ready to sync`);
debug.textContent += `First job preview:\n`;
if (rows[0]) {
debug.textContent += `Job_Number: ${rows[0].Job_Number}\n`;
debug.textContent += `Job_Full_Name: ${rows[0].Job_Full_Name}\n`;
debug.textContent += `Active: ${rows[0].Active}\n`;
debug.textContent += `pb_id (for updates): ${rows[0].id}\n`;
}
syncBtn.classList.add('ready');
syncBtn.disabled = false;
}
// ================================================
// 7. LOGIN — Turns green when successful
// ================================================
loginBtn.onclick = async () => {
const url = pbUrl.value.trim();
if (!url || !emailInput.value || !pass.value) return showError('Fill all fields');
loginBtn.disabled = true;
loginBtn.textContent = 'Logging in...';
try {
pb = new PocketBase(url);
await pb.collection('_superusers').authWithPassword(emailInput.value, pass.value);
loginBtn.classList.add('logged-in');
loginBtn.textContent = 'Logged In';
showSuccess('Logged in successfully');
syncBtn.disabled = false;
deleteAllBtn.disabled = false;
} catch (e) {
showError('Login failed: ' + e.message);
loginBtn.textContent = '1. Login to PocketBase';
} finally {
loginBtn.disabled = false;
}
};
// ================================================
// 8. SYNC — Green when ready, stays green when done
// ================================================
syncBtn.onclick = async () => {
if (!tableData.length) return showError('Load Excel first');
syncBtn.classList.remove('ready');
syncBtn.classList.add('syncing');
syncBtn.textContent = 'Syncing...';
showStatus('Syncing jobs...');
let created = 0, updated = 0;
try {
// ------------------------------
// ORIGINAL: Get all existing PB records
// ------------------------------
const existing = await pb.collection(COLLECTION).getFullList();
const idMap = new Map(existing.map(r => [r.id, r]));
// Additional: Also map PocketBase by Job_Number
// This prevents duplicates when Excel has no pb_id
const jobMap = new Map(
existing
.filter(r => r.Job_Number != null && String(r.Job_Number).trim() !== "")
.map(r => [String(r.Job_Number).trim(), r])
);
// ------------------------------
// Process each Excel row
// ------------------------------
for (const row of tableData) {
const pbId = row.id; // Excel pb_id, may be null
const excelJob = row.Job_Number ? String(row.Job_Number).trim() : null;
delete row.id; // Remove id from payload
// Skip truly empty rows
if (Object.values(row).every(v => v === null || v === '')) continue;
try {
// ------------------------------------------------------------
// PRIORITY 1: Update by pb_id
// ------------------------------------------------------------
if (pbId && idMap.has(pbId)) {
await pb.collection(COLLECTION).update(pbId, row);
updated++;
continue;
}
// ------------------------------------------------------------
// PRIORITY 2: Update by Job_Number
// Only used when pb_id is missing
// Prevents duplicate creation
// ------------------------------------------------------------
if (excelJob && jobMap.has(excelJob)) {
const match = jobMap.get(excelJob);
await pb.collection(COLLECTION).update(match.id, row);
updated++;
continue;
}
// ------------------------------------------------------------
// PRIORITY 3 → Otherwise create a new record
// ------------------------------------------------------------
await pb.collection(COLLECTION).create(row);
created++;
} catch (e) {
debug.textContent += `Row error: ${e.message}\n`;
}
}
showSuccess(`Sync complete — ${created} created, ${updated} updated`);
syncBtn.classList.add('done');
syncBtn.textContent = 'Sync Complete';
} catch (e) {
showError('Sync failed: ' + e.message);
}
};
// ================================================
// 9. DELETE ALL — Danger button
// ================================================
deleteAllBtn.onclick = async () => {
if (!confirm('Delete EVERY record in Job_Info')) return;
if (prompt('Type DELETE to confirm permanent deletion:') !== 'DELETE') {
showError('Cancelled');
return;
}
deleteAllBtn.disabled = true;
deleteAllBtn.textContent = 'Deleting...';
try {
const list = await pb.collection(COLLECTION).getFullList();
for (let i = 0; i < list.length; i += 50) {
await Promise.all(list.slice(i, i + 50).map(r => pb.collection(COLLECTION).delete(r.id)));
}
showSuccess(`Deleted all ${list.length} records`);
} catch (e) {
showError('Delete failed: ' + e.message);
} finally {
deleteAllBtn.disabled = false;
deleteAllBtn.textContent = 'DELETE ALL RECORDS (DANGER)';
}
};
// ================================================
// 10. UI HELPERS
// ================================================
function showStatus(m) { status.className = 'out'; status.textContent = m; }
function showSuccess(m) { status.className = 'out success'; status.textContent = m; }
function showError(m) { status.className = 'out error'; status.textContent = m; }
</script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Auth Response</title>
</head>
<body>
<p>Processing login...</p>
<script src="https://alcdn.msauth.net/browser/2.36.1/js/msal-browser.min.js"></script>
<script type="module">
import { initializeAuth } from "/src/auth.js";
import { log } from "/src/logger.js";
log("AUTH-RESPONSE page loaded.");
initializeAuth().then(() => {
log("Redirect handling complete. Returning to app...");
// After MSAL processes tokens, return to the main app
window.location.href = "/";
});
</script>
</body>
</html>
+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Job Info — Vanilla JS</title>
<style>
/* Integrated CSS for layout, cards, notes, impersonation banner as in your working version */
body { font-family: sans-serif; margin:0; background:#f3f4f6; }
#impersonationBanner { display:none; position:fixed; top:0; left:0; right:0; background:#ffcc00; color:#000; padding:10px; font-weight:bold; text-align:center; z-index:9999; }
#impersonationBanner button { margin-left:10px; padding:4px 8px; cursor:pointer; }
/* other CSS omitted for brevity, keep integrated as in your working file */
</style>
</head>
<body>
<div id="impersonationBanner">
⚠️ Impersonation Mode Active — All actions are logged as the impersonated user
<button id="exitImpersonation">Return to Admin</button>
</div>
<div class="wrap">
<h1>Job Info</h1>
<div class="progress-wrap"><div id="progressBar" class="progress-bar"></div></div>
<input id="searchBox" placeholder="Search jobs">
<button id="filterToggle">Show Filters</button>
<div id="filtersPanel">
<!-- Filters as in working version -->
</div>
<div id="results"></div>
</div>
<div id="detailModal" role="dialog" aria-modal="true">
<div id="detailBox">
<h2 style="margin-top:0;color:#0d6efd">Job Detail</h2>
<table class="detail-table" id="detailTable"></table>
<div id="jobFolderLink"><strong>Job Folder Link</strong><div><span id="jobFolderAnchor">No Link Available</span></div></div>
<h3>Notes</h3>
<div id="jobNotes"></div>
<div class="toolbar" id="toolbar">
<!-- Toolbar buttons -->
</div>
<div id="newNote" contenteditable="true" aria-label="New note"></div>
<div class="modal-actions">
<button class="btn green" id="btnSubmit">Submit</button>
<button class="btn gray" id="btnClearNote">Clear</button>
<button class="btn red" id="btnClose">Close</button>
</div>
</div>
</div>
<script>
(async function() {
// --- Tokens & impersonation ---
const impersonationToken = localStorage.getItem('pb_token');
const originalToken = localStorage.getItem('pb_original_token');
const banner = document.getElementById('impersonationBanner');
if (impersonationToken) {
if (window.pb) window.pb.authStore.save(impersonationToken, null);
banner.style.display = 'block';
}
document.getElementById('exitImpersonation')?.addEventListener('click', () => {
if (originalToken) {
localStorage.setItem('pb_token', originalToken);
localStorage.removeItem('pb_original_token');
} else {
localStorage.removeItem('pb_token');
}
banner.style.display = 'none';
location.reload();
});
// --- Fetch jobs, render, filters, notes, toolbar logic ---
// Use the same logic from your working index.html
})();
</script>
</body>
</html>
+401
View File
@@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
<title>Job Info — Vanilla JS</title>
<style>
/*impersonate styling*/
body { font-family: sans-serif; margin: 0; }
#impersonationBanner {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ffcc00;
color: #000;
padding: 10px;
font-weight: bold;
text-align: center;
z-index: 9999;
display: none;
}
#impersonationBanner button {
margin-left: 10px;
padding: 4px 8px;
cursor: pointer;
}
/* Basic layout */
:root { --accent:#0d6efd; --muted:#6b7280; --card-bg:#ffffff; --card-border:#d1e7ff; }
body{font-family:Inter,Arial,Helvetica,sans-serif;background:#f3f4f6;margin:0;color:#111; overflow-x: hidden; touch-action: pan-y;}
.wrap{max-width:100%-5px;box-sizing:border-box;margin:18px auto;padding:16px;font-size:17px;}
h1{margin:0 0 12px;text-align:center;color:var(--accent);font-size:33px;font-weight:700;}
/* Progress */
.progress-wrap{width:100%;background:#e6edf7;height:6px;border-radius:4px;overflow:hidden;margin-bottom:12px;}
.progress-bar{height:100%;width:0;background:var(--accent);transition:width .18s linear;}
/* Search + filter toggle */
#searchBox{width:100%;padding:10px;border-radius:8px;border:1px solid #d1d5db;margin-bottom:8px;box-sizing:border-box;font-size: 18px;}
#searchBox::placeholder{color:#9ca3af;}
#filterToggle{background:var(--accent);color:#fff;padding:8px 10px;border-radius:8px;border:none;cursor:pointer;margin-bottom:8px;display:inline-block;}
/* Filters panel */
#filtersPanel{display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;margin-bottom:10px;}
.filter-group{margin-bottom:8px;font-size:14px;}
.filter-group label{margin-right:8px;cursor:pointer;color:var(--muted);}
/* Results/cards */
#results{display:flex;flex-direction:column;gap:12px;max-height:56vh;overflow:auto;padding:4px;}
.card{background:var(--card-bg);border-radius:10px;padding:10px;border:1px solid #e6f0ff;box-shadow:0 6px 14px rgba(14,30,37,0.04);cursor:pointer;position:relative;display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;gap:4px;}
.card-row{display:flex;align-items:center;gap:8px;font-size:18px;line-height:1.2;margin-bottom:2px;}
.card-label{font-weight:700;color:#374151;min-width:72px;white-space:nowrap;}
.card-info{color:#111;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}
.active-dot{position:absolute;top:10px;right:10px;width:14px;height:14px;border-radius:999px;border:2px solid #fff;box-shadow:0 1px 2px rgba(0,0,0,0.12);}
/* Modal */
#detailModal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);align-items:flex-start;justify-content:center;padding-top:2vh;z-index:999;}
#detailBox{margin-block-start:0;margin-block-end:0;background:#fff;border-radius:10px;padding:2px 12px 6px 10px;max-width:380px;width:92%;max-height:84vh;overflow:auto;box-sizing:border-box;}
.h2{margin-top:0; margin-bottom:2px;margin-block-end:0;}
.detail-table{margin-top: 0;width:100%;border-collapse:collapse;margin-bottom:4px;}
.detail-table td{border:1px solid #eef2ff;padding:0px 6px 6px 6px;font-size:13px;vertical-align:top;}
.detail-table td.label{width:40%;background:#fbfdff;font-weight:700;color:#374151;}
#jobFolderLink{margin-bottom:10px;font-size:13px;}
#jobFolderLink a, #jobFolderLink span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* Notes */
#jobNotes{min-height:120px;max-height:200px;overflow:auto;padding:4px;border:1px solid #e6e9ef;border-radius:4px;font-size:11px;background:transparent;}
#newNote{min-height:56px;max-height:120px;overflow:auto;padding:8px;border:1px solid #d7dbe3;border-radius:8px;font-size:16px;background:transparent;margin-top:6px;}
.toolbar{display:flex;flex-wrap:wrap;margin-bottom:3px;margin-top:5px} /* small space below toolbar */
.toolbar button{
height:28px;
width:32px;
font-size:12px;
padding:0 4px;
border-radius:6px;
border:1px solid #ccc;
background:#ccc;
color:#111;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
margin: 1px;
}
.toolbar button#tbClear{
width:auto;
padding:4px 8px;
background:#9ca3af; /* slightly darker gray */
color:#fff;
}
.toolbar button.active{
background:#d1e7ff;
color:#111;
border-color:#0d6efd;
}
.modal-actions{display:flex;gap:8px;margin-top:6px;margin-bottom:50px;}
.btn{padding:8px 10px;border-radius:8px;border:none;color:#fff;cursor:pointer;}
.btn.green{background:#16a34a;} .btn.gray{background:#6b7280;} .btn.red{background:#dc2626;}
/* responsive small */
@media (max-width:380px){ .wrap{padding:12px;} .card{height:98px;} }
</style>
</head>
<body>
<!-- Load MSAL from CDN (required for vanilla) -->
<script src="https://alcdn.msauth.net/browser/2.36.1/js/msal-browser.min.js"></script>
<script type="module" src="/src/app.js"></script>
<!-- Banner UI for impersonation mode -->
<div id="impersonationBanner">
⚠️ Impersonation Mode Active Actions are logged as the impersonated user
<button id="exitImpersonation">Return to Admin</button>
</div>
<div class="wrap">
<h1>Job Info</h1>
<div class="progress-wrap">
<div id="progressBar" class="progress-bar"></div>
</div>
<input id="searchBox" placeholder="Search jobs (just start typing)">
<button id="filterToggle">Show Filters</button>
<div id="filtersPanel">
<div class="filter-group">
<strong>Division</strong><br/>
<label><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
<label><input type="checkbox" class="filter-division" value="R#"> Residential</label>
<label><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
</div>
<div class="filter-group">
<strong>Active</strong><br/>
<label><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
<label><input type="checkbox" class="filter-active" value="No"> No</label>
</div>
<div class="filter-group">
<strong>Status</strong><br/>
<label><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
<label><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
<label><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
<label><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
<label><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
<label><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
<label><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
</div>
<button id="clearFilters" style="background:#6b7280;color:white;padding:8px;border-radius:8px;border:none;">Clear Filters</button>
</div>
<div id="results" aria-live="polite"></div>
</div>
<!-- Detail Modal -->
<div id="detailModal" role="dialog" aria-modal="true">
<div id="detailBox">
<h2 style="margin-top:0;color:var(--accent)">Job Detail</h2>
<table class="detail-table" id="detailTable"></table>
<div id="jobFolderLink"><strong>Job Folder Link</strong><div><span id="jobFolderAnchor">No Link Available</span></div></div>
<h3 style="margin:8px 0 6px 0">Notes</h3>
<div id="jobNotes"></div>
<div class="toolbar" id="toolbar">
<button data-cmd="bold" id="tbBold"><strong>B</strong></button>
<button data-cmd="italic" id="tbItalic"><em>I</em></button>
<button data-cmd="underline" id="tbUnderline"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" style="color:#ef4444">A</button>
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" style="color:#2563eb">A</button>
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" style="color:#000">A</button>
<button data-cmd="removeFormat" id="tbClear">Reset</button>
</div>
<div id="newNote" contenteditable="true" aria-label="New note"></div>
<div class="modal-actions">
<button class="btn green" id="btnSubmit">Submit</button>
<button class="btn gray" id="btnClearNote">Clear</button>
<button class="btn red" id="btnClose">Close</button>
</div>
</div>
</div>
<div id="impersonationBanner" style="
display:none; position:fixed; top:0; left:0; right:0;
background:#ffcc00; color:#000; padding:10px; font-weight:bold;
text-align:center; z-index:9999;">
⚠️ Impersonation Mode Active — All actions are logged as the impersonated user
<button id="exitImpersonation"
style="margin-left:10px; padding:4px 8px; cursor:pointer;">Return to Admin</button>
</div>
<script>
(async function() {
// --- Tokens & impersonation ---
const impersonationToken = localStorage.getItem('pb_token');
const originalToken = localStorage.getItem('pb_original_token');
const banner = document.getElementById('impersonationBanner');
if (impersonationToken) {
if (window.pb) window.pb.authStore.save(impersonationToken, null);
banner.style.display = 'block';
}
// Exit impersonation button
document.getElementById('exitImpersonation')?.addEventListener('click', () => {
if (originalToken) {
localStorage.setItem('pb_token', originalToken);
localStorage.removeItem('pb_original_token');
} else {
localStorage.removeItem('pb_token');
}
banner.style.display = 'none';
location.reload();
});
window.addEventListener('keydown', e => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'i') {
document.getElementById('exitImpersonation')?.click();
}
});
// --- Utility ---
const fetchNoCache = (url, options={}) => fetch(url + `&_ts=${Date.now()}`, options);
const PB_COLLECTION_URL = "http://10.10.1.109:8080/api/collections/pbc_1431294567/records";
const perPage = 50;
let jobsCache = [], visibleJobs = [], currentJob = null;
const progressBar = document.getElementById('progressBar');
const searchBox = document.getElementById('searchBox');
const resultsEl = document.getElementById('results');
const filtersPanel = document.getElementById('filtersPanel');
const filterToggle = document.getElementById('filterToggle');
const detailModal = document.getElementById('detailModal');
const detailTable = document.getElementById('detailTable');
const jobFolderAnchor = document.getElementById('jobFolderAnchor');
const jobNotes = document.getElementById('jobNotes');
const newNote = document.getElementById('newNote');
const toolbar = document.getElementById('toolbar');
const tbButtons = Array.from(toolbar.querySelectorAll('button'));
const safeLower = v => (v||'').toString().trim().toLowerCase();
let prog=0, progTimer=null;
function startProgress(){
prog=0; progressBar.style.width='0%';
if(progTimer) clearInterval(progTimer);
progTimer=setInterval(()=>{
if(prog<85){ prog+=Math.random()*6; progressBar.style.width=Math.min(85,prog).toFixed(2)+'%'; }
else clearInterval(progTimer);
},180);
}
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.style.display='none';},260); }
// --- Fetch all jobs ---
async function fetchAllJobs(){
startProgress(); jobsCache=[]; let page=1, totalItems=0;
try{
while(true){
const url = `${PB_COLLECTION_URL}?page=${page}&perPage=${perPage}`;
const res = await fetchNoCache(url);
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
const data = await res.json();
const items = data.items || [];
jobsCache.push(...items);
totalItems = data.totalItems || (items.length + ((page-1)*perPage));
progressBar.style.width = Math.min(95, Math.floor((jobsCache.length / Math.max(1,totalItems)) * 95)) + '%';
if(jobsCache.length >= totalItems || items.length===0) break;
page++;
}
finishProgress();
applyFiltersAndRender();
}catch(err){ finishProgress(); alert('Failed to fetch jobs: '+err.message); console.error(err);}
}
function applyFiltersAndRender(){
const q = safeLower(searchBox.value||'');
const divisionVals = Array.from(document.querySelectorAll('.filter-division:checked')).map(i=>i.value);
const activeVals = Array.from(document.querySelectorAll('.filter-active:checked')).map(i=>i.value);
const statusVals = Array.from(document.querySelectorAll('.filter-status:checked')).map(i=>i.value);
visibleJobs = jobsCache.filter(job=>{
const searchable = [job.Job_Number, job.Job_Full_Name, job.Job_Address, job.Contact_Person, job.Company_Client, job.Notes].join(' ').toLowerCase();
if(q && !searchable.includes(q)) return false;
if(divisionVals.length && !divisionVals.includes(job.Job_Division)) return false;
if(activeVals.length && !activeVals.includes(job.Active)) return false;
if(statusVals.length && !statusVals.includes(job.Job_Status||'')) return false;
return true;
});
renderCards(visibleJobs);
}
function renderCards(list){
resultsEl.innerHTML='';
if(!list.length){ resultsEl.innerHTML = `<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">No results</div>`; return; }
for(const job of list){
const card=document.createElement('div'); card.className='card';
card.innerHTML=`
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div>
`;
const dot=document.createElement('div'); dot.className='active-dot';
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job));
resultsEl.appendChild(card);
}
}
function escapeHtml(str){ return String(str).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
// --- Filters ---
filterToggle.addEventListener('click',()=>{ filtersPanel.style.display = filtersPanel.style.display==='block'?'none':'block'; filterToggle.textContent = filtersPanel.style.display==='block'?'Hide Filters':'Show Filters'; });
searchBox.addEventListener('input',()=>applyFiltersAndRender());
document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{
if(cb.checked) document.querySelectorAll('.filter-active').forEach(o=>{if(o!==cb)o.checked=false;}); applyFiltersAndRender();
});});
document.querySelectorAll('.filter-status').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
document.getElementById('clearFilters').addEventListener('click',()=>{
document.querySelectorAll('.filter-division, .filter-active, .filter-status').forEach(cb=>cb.checked=false);
applyFiltersAndRender();
});
// --- Modal & Notes ---
function openDetail(job){
currentJob = job;
detailTable.innerHTML = '';
const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"];
for(const f of fields){
const tr = document.createElement('tr');
const tdLabel = document.createElement('td'); tdLabel.className='label'; tdLabel.textContent=f.replace(/_/g,' ');
const tdVal = document.createElement('td'); tdVal.textContent=job[f]||'';
tr.appendChild(tdLabel); tr.appendChild(tdVal); detailTable.appendChild(tr);
}
// Job folder link
jobFolderAnchor.innerHTML = '';
if(job.Job_Folder_Link && String(job.Job_Folder_Link).trim()!==''){
try{ const url=new URL(job.Job_Folder_Link); const a=document.createElement('a'); a.href=url.href; a.target='_blank'; a.rel='noopener noreferrer'; a.textContent=url.href; a.style.color='#0d6efd'; a.style.textDecoration='underline'; jobFolderAnchor.appendChild(a);
}catch(e){ jobFolderAnchor.textContent='No Link Available'; jobFolderAnchor.style.color='#374151'; jobFolderAnchor.style.textDecoration='none'; }
} else { jobFolderAnchor.textContent='No Link Available'; jobFolderAnchor.style.color='#374151'; jobFolderAnchor.style.textDecoration='none'; }
jobNotes.innerHTML = job.Notes || '';
newNote.innerHTML = '';
tbButtons.forEach(b=>b.classList.remove('active'));
detailModal.style.display='flex';
}
document.getElementById('btnClose').addEventListener('click',()=>{detailModal.style.display='none';});
// --- Toolbar ---
let savedSelection = null;
function saveSelection(){ const sel=window.getSelection(); if(sel.rangeCount>0)savedSelection=sel.getRangeAt(0);}
function restoreSelection(){ const sel=window.getSelection(); if(savedSelection){ sel.removeAllRanges(); sel.addRange(savedSelection);}}
newNote.addEventListener('mouseup',saveSelection); newNote.addEventListener('keyup',saveSelection); newNote.addEventListener('focus',saveSelection);
tbButtons.forEach(btn=>{
btn.addEventListener('mousedown', e=>{
e.preventDefault();
restoreSelection();
const cmd = btn.dataset.cmd;
if(!cmd) return;
if(cmd==='foreColor'){ const val=btn.dataset.val; document.execCommand(cmd,false,val); tbButtons.forEach(b=>{if(b.dataset.cmd==='foreColor'&&b!==btn)b.classList.remove('active');}); btn.classList.toggle('active'); }
else if(cmd==='removeFormat'){ restoreSelection(); const sel=window.getSelection(); if(sel.rangeCount>0){ const range=sel.getRangeAt(0); let content=range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(content)); range.setStartAfter(range.startContainer); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } tbButtons.forEach(b=>b.classList.remove('active')); }
else{ document.execCommand(cmd,false,null); btn.classList.toggle('active'); }
newNote.focus(); saveSelection();
});
});
// --- Submit Note ---
document.getElementById('btnSubmit').addEventListener('click', async()=>{
const html=newNote.innerHTML.trim(); if(!html){alert('Enter note text'); return;}
const timestamp=new Date().toLocaleString();
const currentUserName = window.pb?.authStore?.model?.display_name || 'Unknown';
const formatted=`<p><strong>[${currentUserName} ${timestamp}]</strong> ${html}</p>`;
const updatedNotes = formatted + (currentJob.Notes||'');
try{
const res = await fetch(`${PB_COLLECTION_URL}/${currentJob.id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({Notes:updatedNotes})});
if(!res.ok) throw new Error('Update failed: '+res.status);
const updated=await res.json();
const idx=jobsCache.findIndex(j=>j.id===currentJob.id); if(idx>=0) jobsCache[idx].Notes=updated.Notes; currentJob.Notes=updated.Notes;
jobNotes.innerHTML=updated.Notes; newNote.innerHTML='';
applyFiltersAndRender();
}catch(err){alert('Error saving note: '+err.message); console.error(err);}
});
document.getElementById('btnClearNote').addEventListener('click',()=>newNote.innerHTML='');
// --- Init ---
fetchAllJobs();
})();
</script>
v1.0.2
</body>
</html>
@@ -0,0 +1,404 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
<title>Job Info — Vanilla JS</title>
<style>
/* Basic layout */
:root { --accent:#0d6efd; --muted:#6b7280; --card-bg:#ffffff; --card-border:#d1e7ff; }
body{font-family:Inter,Arial,Helvetica,sans-serif;background:#f3f4f6;margin:0;color:#111; overflow-x: hidden; touch-action: pan-y;}
.wrap{max-width:100%-5px;box-sizing:border-box;margin:18px auto;padding:16px;font-size:17px;}
h1{margin:0 0 12px;text-align:center;color:var(--accent);font-size:33px;font-weight:700;}
/* Progress */
.progress-wrap{width:100%;background:#e6edf7;height:6px;border-radius:4px;overflow:hidden;margin-bottom:12px;}
.progress-bar{height:100%;width:0;background:var(--accent);transition:width .18s linear;}
/* Search + filter toggle */
#searchBox{width:100%;padding:10px;border-radius:8px;border:1px solid #d1d5db;margin-bottom:8px;box-sizing:border-box;font-size: 18px;}
#searchBox::placeholder{color:#9ca3af;}
#filterToggle{background:var(--accent);color:#fff;padding:8px 10px;border-radius:8px;border:none;cursor:pointer;margin-bottom:8px;display:inline-block;}
/* Filters panel */
#filtersPanel{display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;margin-bottom:10px;}
.filter-group{margin-bottom:8px;font-size:14px;}
.filter-group label{margin-right:8px;cursor:pointer;color:var(--muted);}
/* Results/cards */
#results{display:flex;flex-direction:column;gap:12px;max-height:56vh;overflow:auto;padding:4px;}
.card{background:var(--card-bg);border-radius:10px;padding:10px;border:1px solid #e6f0ff;box-shadow:0 6px 14px rgba(14,30,37,0.04);cursor:pointer;position:relative;display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;gap:4px;}
.card-row{display:flex;align-items:center;gap:8px;font-size:18px;line-height:1.2;margin-bottom:2px;}
.card-label{font-weight:700;color:#374151;min-width:72px;white-space:nowrap;}
.card-info{color:#111;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}
.active-dot{position:absolute;top:10px;right:10px;width:14px;height:14px;border-radius:999px;border:2px solid #fff;box-shadow:0 1px 2px rgba(0,0,0,0.12);}
/* Modal */
#detailModal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);align-items:flex-start;justify-content:center;padding-top:2vh;z-index:999;}
#detailBox{margin-block-start:0;margin-block-end:0;background:#fff;border-radius:10px;padding:2px 12px 6px 10px;max-width:380px;width:92%;max-height:84vh;overflow:auto;box-sizing:border-box;}
.h2{margin-top:0; margin-bottom:2px;margin-block-end:0;}
.detail-table{margin-top: 0;width:100%;border-collapse:collapse;margin-bottom:4px;}
.detail-table td{border:1px solid #eef2ff;padding:0px 6px 6px 6px;font-size:13px;vertical-align:top;}
.detail-table td.label{width:40%;background:#fbfdff;font-weight:700;color:#374151;}
#jobFolderLink{margin-bottom:10px;font-size:13px;}
#jobFolderLink a, #jobFolderLink span{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
/* Notes */
#jobNotes{min-height:120px;max-height:200px;overflow:auto;padding:4px;border:1px solid #e6e9ef;border-radius:4px;font-size:11px;background:transparent;}
#newNote{min-height:56px;max-height:120px;overflow:auto;padding:8px;border:1px solid #d7dbe3;border-radius:8px;font-size:16px;background:transparent;margin-top:6px;}
.toolbar{display:flex;flex-wrap:wrap;margin-bottom:3px;margin-top:5px} /* small space below toolbar */
.toolbar button{
height:28px;
width:32px;
font-size:12px;
padding:0 4px;
border-radius:6px;
border:1px solid #ccc;
background:#ccc;
color:#111;
cursor:pointer;
display:flex;
align-items:center;
justify-content:center;
margin: 1px;
}
.toolbar button#tbClear{
width:auto;
padding:4px 8px;
background:#9ca3af; /* slightly darker gray */
color:#fff;
}
.toolbar button.active{
background:#d1e7ff;
color:#111;
border-color:#0d6efd;
}
.modal-actions{display:flex;gap:8px;margin-top:6px;margin-bottom:50px;}
.btn{padding:8px 10px;border-radius:8px;border:none;color:#fff;cursor:pointer;}
.btn.green{background:#16a34a;} .btn.gray{background:#6b7280;} .btn.red{background:#dc2626;}
/* responsive small */
@media (max-width:380px){ .wrap{padding:12px;} .card{height:98px;} }
</style>
</head>
<body>
<div class="wrap">
<h1>Job Info</h1>
<div class="progress-wrap">
<div id="progressBar" class="progress-bar"></div>
</div>
<input id="searchBox" placeholder="Search jobs (just start typing)">
<button id="filterToggle">Show Filters</button>
<div id="filtersPanel">
<div class="filter-group">
<strong>Division</strong><br/>
<label><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
<label><input type="checkbox" class="filter-division" value="R#"> Residential</label>
<label><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
</div>
<div class="filter-group">
<strong>Active</strong><br/>
<label><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
<label><input type="checkbox" class="filter-active" value="No"> No</label>
</div>
<div class="filter-group">
<strong>Status</strong><br/>
<label><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
<label><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
<label><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
<label><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
<label><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
<label><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
<label><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
</div>
<button id="clearFilters" style="background:#6b7280;color:white;padding:8px;border-radius:8px;border:none;">Clear Filters</button>
</div>
<div id="results" aria-live="polite"></div>
</div>
<!-- Detail Modal -->
<div id="detailModal" role="dialog" aria-modal="true">
<div id="detailBox">
<h2 style="margin-top:0;color:var(--accent)">Job Detail</h2>
<table class="detail-table" id="detailTable"></table>
<div id="jobFolderLink"><strong>Job Folder Link</strong><div><span id="jobFolderAnchor">No Link Available</span></div></div>
<h3 style="margin:8px 0 6px 0">Notes</h3>
<div id="jobNotes"></div>
<div class="toolbar" id="toolbar">
<button data-cmd="bold" id="tbBold"><strong>B</strong></button>
<button data-cmd="italic" id="tbItalic"><em>I</em></button>
<button data-cmd="underline" id="tbUnderline"><u>U</u></button>
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" style="color:#ef4444">A</button>
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" style="color:#2563eb">A</button>
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" style="color:#000">A</button>
<button data-cmd="removeFormat" id="tbClear">Reset</button>
</div>
<div id="newNote" contenteditable="true" aria-label="New note"></div>
<div class="modal-actions">
<button class="btn green" id="btnSubmit">Submit</button>
<button class="btn gray" id="btnClearNote">Clear</button>
<button class="btn red" id="btnClose">Close</button>
</div>
</div>
</div>
<script>
/* Force Refresh so jobs aren't stuck in local storage cache */
const fetchNoCache = (url, options={}) => fetch(url + `&_ts=${Date.now()}`, options);
/* Set location where the pocketbase tables are */
const PB_COLLECTION_URL = "http://10.10.1.47:8080/api/collections/pbc_1431294567/records";
const perPage = 50;
let jobsCache = [], visibleJobs = [], currentJob = null;
/* DOM refs */
const progressBar = document.getElementById('progressBar');
const searchBox = document.getElementById('searchBox');
const resultsEl = document.getElementById('results');
const filtersPanel = document.getElementById('filtersPanel');
const filterToggle = document.getElementById('filterToggle');
const detailModal = document.getElementById('detailModal');
const detailTable = document.getElementById('detailTable');
const jobFolderAnchor = document.getElementById('jobFolderAnchor');
const jobNotes = document.getElementById('jobNotes');
const newNote = document.getElementById('newNote');
const toolbar = document.getElementById('toolbar');
const tbButtons = Array.from(toolbar.querySelectorAll('button'));
function safeLower(v){ return (v||'').toString().trim().toLowerCase(); }
let prog=0, progTimer=null;
function startProgress(){
prog=0; progressBar.style.width='0%';
if(progTimer) clearInterval(progTimer);
progTimer=setInterval(()=>{
if(prog<85){ prog+=Math.random()*6; progressBar.style.width=Math.min(85,prog).toFixed(2)+'%'; }
else clearInterval(progTimer);
},180);
}
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.style.display='none';},260); }
async function fetchAllJobs(){
startProgress(); jobsCache=[]; let page=1, totalItems=0;
try{
while(true){
const url = `${PB_COLLECTION_URL}?page=${page}&perPage=${perPage}`;
const res = await fetchNoCache(url);
if(!res.ok) throw new Error(`Fetch failed: ${res.status}`);
const data = await res.json();
const items = data.items || [];
jobsCache.push(...items);
totalItems = data.totalItems || (items.length + ((page-1)*perPage));
progressBar.style.width = Math.min(95, Math.floor((jobsCache.length / Math.max(1,totalItems)) * 95)) + '%';
if(jobsCache.length >= totalItems || items.length===0) break;
page++;
}
finishProgress();
applyFiltersAndRender();
}catch(err){ finishProgress(); alert('Failed to fetch jobs: '+err.message); console.error(err);}
}
function applyFiltersAndRender(){
const q = safeLower(searchBox.value||'');
const divisionVals = Array.from(document.querySelectorAll('.filter-division:checked')).map(i=>i.value);
const activeVals = Array.from(document.querySelectorAll('.filter-active:checked')).map(i=>i.value);
const statusVals = Array.from(document.querySelectorAll('.filter-status:checked')).map(i=>i.value);
visibleJobs = jobsCache.filter(job=>{
const searchable = [job.Job_Number, job.Job_Full_Name, job.Job_Address, job.Contact_Person, job.Company_Client, job.Notes].join(' ').toLowerCase();
if(q && !searchable.includes(q)) return false;
if(divisionVals.length && !divisionVals.includes(job.Job_Division)) return false;
if(activeVals.length && !activeVals.includes(job.Active)) return false;
if(statusVals.length && !statusVals.includes(job.Job_Status||'')) return false;
return true;
});
renderCards(visibleJobs);
}
function renderCards(list){
resultsEl.innerHTML='';
if(!list.length){ resultsEl.innerHTML = `<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">No results</div>`; return; }
for(const job of list){
const card=document.createElement('div'); card.className='card';
card.innerHTML=`
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div>
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div>
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div>
`;
const dot=document.createElement('div'); dot.className='active-dot';
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
card.appendChild(dot);
card.addEventListener('click',()=>openDetail(job));
resultsEl.appendChild(card);
}
}
function escapeHtml(str){ return String(str).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;'); }
/* Filters */
filterToggle.addEventListener('click',()=>{
filtersPanel.style.display = filtersPanel.style.display==='block'?'none':'block';
filterToggle.textContent = filtersPanel.style.display==='block'?'Hide Filters':'Show Filters';
});
searchBox.addEventListener('input',()=>applyFiltersAndRender());
document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{
if(cb.checked) document.querySelectorAll('.filter-active').forEach(o=>{if(o!==cb)o.checked=false;}); applyFiltersAndRender();
});});
document.querySelectorAll('.filter-status').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
document.getElementById('clearFilters').addEventListener('click',()=>{
document.querySelectorAll('.filter-division, .filter-active, .filter-status').forEach(cb=>cb.checked=false);
applyFiltersAndRender();
});
/* Modal & Notes */
function openDetail(job){
currentJob = job;
detailTable.innerHTML = '';
const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"];
for(const f of fields){
const tr = document.createElement('tr');
const tdLabel = document.createElement('td');
tdLabel.className = 'label';
tdLabel.textContent = f.replace(/_/g,' ');
const tdVal = document.createElement('td');
tdVal.textContent = job[f] || '';
tr.appendChild(tdLabel);
tr.appendChild(tdVal);
detailTable.appendChild(tr);
}
// Handle Job Folder Link
jobFolderAnchor.innerHTML = ''; // clear previous content
if(job.Job_Folder_Link && String(job.Job_Folder_Link).trim() !== ''){
try {
const url = new URL(job.Job_Folder_Link);
const a = document.createElement('a');
a.href = url.href;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = url.href;
a.style.color = '#0d6efd';
a.style.textDecoration = 'underline';
jobFolderAnchor.appendChild(a);
} catch(e){
// Invalid URL
jobFolderAnchor.textContent = 'No Link Available';
jobFolderAnchor.style.color = '#374151';
jobFolderAnchor.style.textDecoration = 'none';
}
} else {
jobFolderAnchor.textContent = 'No Link Available';
jobFolderAnchor.style.color = '#374151';
jobFolderAnchor.style.textDecoration = 'none';
}
jobNotes.innerHTML = job.Notes || '';
newNote.innerHTML = '';
tbButtons.forEach(b => b.classList.remove('active'));
detailModal.style.display = 'flex';
}
document.getElementById('btnClose').addEventListener('click',()=>{detailModal.style.display='none';});
/* Toolbar */
let savedSelection = null;
// Save selection on user actions
function saveSelection() {
const sel = window.getSelection();
if (sel.rangeCount > 0) savedSelection = sel.getRangeAt(0);
}
function restoreSelection() {
const sel = window.getSelection();
if (savedSelection) {
sel.removeAllRanges();
sel.addRange(savedSelection);
}
}
newNote.addEventListener('mouseup', saveSelection);
newNote.addEventListener('keyup', saveSelection);
newNote.addEventListener('focus', saveSelection);
// Toolbar buttons
tbButtons.forEach(btn => {
btn.addEventListener('mousedown', e => {
e.preventDefault(); // prevent button click stealing focus
restoreSelection(); // restore selection before executing command
const cmd = btn.dataset.cmd;
if (!cmd) return;
if (cmd === 'foreColor') {
const val = btn.dataset.val;
document.execCommand(cmd, false, val);
tbButtons.forEach(b => { if (b.dataset.cmd === 'foreColor' && b !== btn) b.classList.remove('active'); });
btn.classList.toggle('active');
} else if (cmd === 'removeFormat') {
restoreSelection();
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
let content = range.toString();
// Replace selection or caret with plain text
range.deleteContents();
const plainNode = document.createTextNode(content);
range.insertNode(plainNode);
// Place caret after inserted text
range.setStartAfter(plainNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
tbButtons.forEach(b => b.classList.remove('active'));
} else {
document.execCommand(cmd, false, null);
btn.classList.toggle('active');
}
newNote.focus();
saveSelection();
});
});
/* Submit note */
document.getElementById('btnSubmit').addEventListener('click', async()=>{
const html=newNote.innerHTML.trim(); if(!html){alert('Enter note text'); return;}
const timestamp=new Date().toLocaleString();
const formatted=`<p><strong>[CurrentUser ${timestamp}]</strong> ${html}</p>`;
const updatedNotes=formatted+(currentJob.Notes||'');
try{
const res=await fetch(`${PB_COLLECTION_URL}/${currentJob.id}`,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({Notes:updatedNotes})});
if(!res.ok) throw new Error('Update failed: '+res.status);
const updated=await res.json();
const idx=jobsCache.findIndex(j=>j.id===currentJob.id); if(idx>=0) jobsCache[idx].Notes=updated.Notes; currentJob.Notes=updated.Notes;
jobNotes.innerHTML=updated.Notes; newNote.innerHTML='';
applyFiltersAndRender();
}catch(err){alert('Error saving note: '+err.message); console.error(err);}
});
document.getElementById('btnClearNote').addEventListener('click',()=>newNote.innerHTML='');
fetchAllJobs();
</script>
</body>
</html>
File diff suppressed because one or more lines are too long
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test Impersonation</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
#banner { display:none; background:#ffcc00; padding:1rem; margin-bottom:1rem; font-weight:bold; }
#form { display:flex; flex-direction:column; max-width:400px; }
label { margin:0.5rem 0 0.2rem; }
input { padding:0.5rem; font-size:1rem; }
button { margin-top:1rem; padding:0.5rem; font-size:1rem; }
</style>
</head>
<body>
<div id="banner">⚠️ Impersonation Mode Active</div>
<h1>Test Impersonation</h1>
<div id="form">
<label for="targetInput">User ID or Email:</label>
<input type="text" id="targetInput" placeholder="Enter user ID or email">
<button id="impersonateBtn">Impersonate</button>
<button id="returnBtn" style="display:none;">Return to Admin</button>
<button id="goMainBtn" style="display:none;">Go to Main App</button>
</div>
<script>
const banner = document.getElementById('banner');
const targetInput = document.getElementById('targetInput');
const impersonateBtn = document.getElementById('impersonateBtn');
const returnBtn = document.getElementById('returnBtn');
const goMainBtn = document.getElementById('goMainBtn');
let originalToken = localStorage.getItem('pb_original_token') || localStorage.getItem('pb_token') || '';
impersonateBtn.addEventListener('click', async () => {
const identifier = targetInput.value.trim();
if (!identifier) return alert('Enter a user ID or email');
if (!originalToken) return alert('You must be logged in as Admin/Superuser');
try {
const res = await fetch('http://localhost:8081/api/impersonate', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + originalToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ targetUserIdentifier: identifier })
});
if (!res.ok) return alert('Failed: ' + await res.text());
const data = await res.json();
localStorage.setItem('pb_token', data.token);
if (!localStorage.getItem('pb_original_token')) {
localStorage.setItem('pb_original_token', originalToken);
}
banner.style.display = 'block';
returnBtn.style.display = 'inline-block';
goMainBtn.style.display = 'inline-block';
impersonateBtn.disabled = true;
alert('Now impersonating ' + identifier);
} catch (err) { alert('Error: ' + err.message); }
});
returnBtn.addEventListener('click', () => {
const orig = localStorage.getItem('pb_original_token');
if (!orig) return alert('Original token not found');
localStorage.setItem('pb_token', orig);
localStorage.removeItem('pb_original_token');
banner.style.display = 'none';
returnBtn.style.display = 'none';
goMainBtn.style.display = 'none';
impersonateBtn.disabled = false;
alert('Returned to Admin session');
});
goMainBtn.addEventListener('click', () => window.location.href = '/index.html');
if (localStorage.getItem('pb_original_token') && localStorage.getItem('pb_token') !== localStorage.getItem('pb_original_token')) {
banner.style.display = 'block';
returnBtn.style.display = 'inline-block';
goMainBtn.style.display = 'inline-block';
impersonateBtn.disabled = true;
}
</script>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
import { initializeAuth, getToken } from "./auth.js";
import { log, error } from "./logger.js";
async function startApp() {
log("App starting...");
await initializeAuth();
try {
const token = await getToken();
log("Final acquired token:", token.slice(0, 30) + "...");
document.querySelector("#content").textContent =
"Logged in successfully. Token acquired.";
} catch (e) {
error("Failed to authenticate:", e);
document.querySelector("#content").textContent =
"Authentication failed. Check console.";
}
}
startApp();
+77
View File
@@ -0,0 +1,77 @@
import { msalInstance, loginRequest } from "./msalConfig.js";
import { log, warn, error } from "./logger.js";
export async function initializeAuth() {
log("Running handleRedirectPromise...");
return msalInstance.handleRedirectPromise().then((response) => {
if (response) {
log("Redirect response received:", response);
if (response.account) {
log("Setting active account from redirect:", response.account.username);
msalInstance.setActiveAccount(response.account);
}
} else {
log("No redirect response (normal page load).");
}
const accounts = msalInstance.getAllAccounts();
log("Existing accounts:", accounts);
if (accounts.length > 0 && !msalInstance.getActiveAccount()) {
log("Setting first account as active:", accounts[0].username);
msalInstance.setActiveAccount(accounts[0]);
}
});
}
export async function getToken() {
const active = msalInstance.getActiveAccount();
log("Starting token acquisition. Active account:", active);
try {
log("Attempting acquireTokenSilent...");
const result = await msalInstance.acquireTokenSilent({
...loginRequest,
account: active
});
log("Silent token success:");
log(" Expires:", new Date(result.expiresOn).toLocaleString());
log(" Scopes:", result.scopes);
log(" Access token (first 20 chars):", result.accessToken.slice(0, 20) + "...");
return result.accessToken;
} catch (err) {
warn("Silent token failed:", err);
// Only do popup if it truly needs human interaction
if (err.name === "InteractionRequiredAuthError" ||
err.errorCode === "interaction_required") {
log("Interaction required → Launching popup login...");
const popup = await msalInstance.loginPopup(loginRequest);
log("Popup login success. Account:", popup.account.username);
msalInstance.setActiveAccount(popup.account);
log("Retrying silent acquisition after popup...");
const refreshed = await msalInstance.acquireTokenSilent({
...loginRequest,
account: popup.account
});
log("Post-popup silent token success:");
log(" Expires:", new Date(refreshed.expiresOn).toLocaleString());
log(" Access token (first 20 chars):", refreshed.accessToken.slice(0, 20) + "...");
return refreshed.accessToken;
}
error("Unexpected token error:", err);
throw err;
}
}
+834
View File
@@ -0,0 +1,834 @@
import React, { useEffect, useState, useRef } from "react";
import {
Text,
Input,
Spinner,
Button,
Dialog,
DialogSurface,
DialogBody,
DialogTitle,
DialogActions,
Menu,
MenuTrigger,
MenuPopover,
MenuList,
MenuItem,
} from "@fluentui/react-components";
import { DeleteRegular, SearchRegular, ChevronDownRegular } from "@fluentui/react-icons";
import type {
GeneratedComponentProps,
ccllc_job,
ReadableTableRow,
QueryTableOptions,
} from "./RuntimeTypes";
/* ──────────────────────────────── DEFAULTS ──────────────────────────────── */
const DEFAULT_COLUMN_ORDER = [
"ccllc_jobnumber", "ccllc_jobname", "ccllc_jobaddress", "ccllc_jobtype",
"ccllc_flagstatus", "ccllc_jobstatus", "ccllc_jobdivision", "ccllc_estimatorname",
"ccllc_officerepresentative", "ccllc_clientcompany", "ccllc_contactperson",
"ccllc_contactphonenumber", "ccllc_contactemail", "ccllc_duedate", "ccllc_duedatecounter",
"ccllc_duetime", "ccllc_documentsuploaded", "ccllc_quickbookscreated", "ccllc_addedtocalendar",
"ccllc_pswiftuploaded", "ccllc_voxercreated", "ccllc_estimatedraft", "ccllc_estimatereviewed",
"ccllc_estimateapproved", "ccllc_notes", "ccllc_activestatus", "ccllc_jobquickbookslink",
"ccllc_voxerlink", "ccllc_jobcodes", "ccllc_submissiondate", "ccllc_powerappsid",
"ccllc_jobfolderlink", "ccllc_taxexemptstatus", "statecode", "statuscode", "ccllc_active",
] as const;
const DEFAULT_FRIENDLY_NAMES: Record<string, string> = {
ccllc_jobnumber: "Job Number",
ccllc_jobname: "Job Name",
statecode: "State",
statuscode: "Status",
ccllc_active: "Key",
ccllc_activestatus: "Active\nStatus",
ccllc_addedtocalendar: "Added to\nCalendar",
ccllc_clientcompany: "Client\nCompany",
ccllc_contactemail: "Contact\nEmail",
ccllc_contactperson: "Contact\nPerson",
ccllc_contactphonenumber: "Contact\nPhone Number",
ccllc_documentsuploaded: "Documents\nUploaded",
ccllc_duedate: "Due Date",
ccllc_duedatecounter: "Due Date\nCounter",
ccllc_duetime: "Due Time",
ccllc_estimateapproved: "Estimate\nApproved",
ccllc_estimatedraft: "Estimate\nDraft",
ccllc_estimatereviewed: "Estimate\nReviewed",
ccllc_estimatorname: "Estimator\nName",
ccllc_flagstatus: "Flag",
ccllc_jobaddress: "Job\nAddress",
ccllc_jobcodes: "Job\nCodes",
ccllc_jobdivision: "Job\nDivision",
ccllc_jobfolderlink: "Job Folder Link",
ccllc_jobquickbookslink: "Job QuickBooks\nLink",
ccllc_jobstatus: "Job\nStatus",
ccllc_jobtype: "Job\nType",
ccllc_notes: "Notes",
ccllc_officerepresentative: "Office Rep",
ccllc_powerappsid: "PowerApps\nID",
ccllc_pswiftuploaded: "PSwift\nUploaded",
ccllc_quickbookscreated: "QB Created",
ccllc_submissiondate: "Submission\nDate",
ccllc_taxexemptstatus: "Tax Exempt",
ccllc_voxercreated: "Voxer\nCreated",
ccllc_voxerlink: "Voxer Link",
};
const DEFAULT_COLUMN_ALIGNMENTS: Record<string, "left" | "center" | "right"> = {
ccllc_jobnumber: "center",
ccllc_jobname: "left",
statecode: "center",
statuscode: "center",
ccllc_active: "center",
ccllc_activestatus: "center",
ccllc_addedtocalendar: "center",
ccllc_clientcompany: "left",
ccllc_contactemail: "left",
ccllc_contactperson: "left",
ccllc_contactphonenumber: "left",
ccllc_documentsuploaded: "center",
ccllc_duedate: "center",
ccllc_duedatecounter: "center",
ccllc_duetime: "center",
ccllc_estimateapproved: "center",
ccllc_estimatedraft: "center",
ccllc_estimatereviewed: "center",
ccllc_estimatorname: "left",
ccllc_flagstatus: "center",
ccllc_jobaddress: "left",
ccllc_jobcodes: "left",
ccllc_jobdivision: "left",
ccllc_jobfolderlink: "left",
ccllc_jobquickbookslink: "left",
ccllc_jobstatus: "center",
ccllc_jobtype: "center",
ccllc_notes: "left",
ccllc_officerepresentative: "left",
ccllc_powerappsid: "center",
ccllc_pswiftuploaded: "center",
ccllc_quickbookscreated: "center",
ccllc_submissiondate: "center",
ccllc_taxexemptstatus: "center",
ccllc_voxercreated: "center",
ccllc_voxerlink: "left",
};
const DEFAULT_COLUMN_WIDTHS: Record<string, number> = {
ccllc_jobnumber: 100,
ccllc_jobname: 280,
statecode: 80,
statuscode: 80,
ccllc_active: 80,
ccllc_activestatus: 100,
ccllc_addedtocalendar: 120,
ccllc_clientcompany: 160,
ccllc_contactemail: 180,
ccllc_contactperson: 140,
ccllc_contactphonenumber: 140,
ccllc_documentsuploaded: 120,
ccllc_duedate: 110,
ccllc_duedatecounter: 110,
ccllc_duetime: 100,
ccllc_estimateapproved: 120,
ccllc_estimatedraft: 120,
ccllc_estimatereviewed: 120,
ccllc_estimatorname: 140,
ccllc_flagstatus: 80,
ccllc_jobaddress: 180,
ccllc_jobcodes: 120,
ccllc_jobdivision: 120,
ccllc_jobfolderlink: 160,
ccllc_jobquickbookslink: 160,
ccllc_jobstatus: 120,
ccllc_jobtype: 120,
ccllc_notes: 180,
ccllc_officerepresentative: 140,
ccllc_powerappsid: 120,
ccllc_pswiftuploaded: 120,
ccllc_quickbookscreated: 120,
ccllc_submissiondate: 120,
ccllc_taxexemptstatus: 120,
ccllc_voxercreated: 120,
ccllc_voxerlink: 160,
};
const DEFAULT_COLUMN_VISIBILITY = Object.fromEntries(
DEFAULT_COLUMN_ORDER.map(c => [c, true])
);
/* ──────────────────────────────── UTILS ──────────────────────────────── */
const getJobColumns = (row?: ReadableTableRow<ccllc_job>): string[] => {
const sticky = ["ccllc_jobnumber", "ccllc_jobname"];
const ignore = new Set(["ccllc_jobid", "@Odata.Etag", "@odata.etag"]);
if (!row) return sticky;
return [
...sticky,
...Object.keys(row).filter(
k =>
!sticky.includes(k) &&
!ignore.has(k) &&
!k.endsWith("@OData.Community.Display.V1.FormattedValue") &&
typeof row[k as keyof typeof row] !== "function"
),
];
};
const humanize = (col: string) =>
col.replace(/^ccllc_/, "").replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase());
/* ───────────────────────────── EditableCell ───────────────────────────── */
function EditableCell({
value,
onSave,
rowId,
colKey,
isSticky,
width,
disabled,
isFirstNonSticky,
align,
}: {
value: string;
onSave: (v: string) => Promise<void>;
rowId: string;
colKey: string;
isSticky?: boolean;
width?: number;
disabled?: boolean;
isFirstNonSticky?: boolean;
align?: "left" | "center" | "right";
}) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(value ?? "");
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
useEffect(() => setEditValue(value ?? ""), [value]);
const startEdit = () => !disabled && setEditing(true);
const finishEdit = async () => {
const final = colKey === "ccllc_jobnumber" ? editValue.replace(/,/g, "") : editValue;
if (final !== value) await onSave(final);
setEditing(false);
};
const cellStyle: React.CSSProperties = {
minWidth: width ? `${width}px` : undefined,
maxWidth: width ? `${width}px` : undefined,
width: width ? `${width}px` : undefined,
borderTop: "1px solid #000",
borderLeft: isFirstNonSticky ? "none" : "1px solid #000",
borderRight: colKey === "ccllc_jobnumber" ? "none" : "1px solid #000",
borderBottom: "1px solid #000",
boxSizing: "border-box",
position: isSticky ? "sticky" : undefined,
left: isSticky && colKey === "ccllc_jobnumber" ? 0 : isSticky && colKey === "ccllc_jobname" ? 100 : undefined,
zIndex: isSticky ? 1500 : undefined,
background: isSticky ? "#fff" : undefined,
overflow: "hidden",
whiteSpace: "nowrap",
padding: "6px 10px",
cursor: disabled ? "not-allowed" : "pointer",
userSelect: "none",
textAlign: align || "left",
};
return (
<td style={cellStyle} onClick={startEdit} tabIndex={disabled ? -1 : 0}>
{editing && !disabled ? (
<input
ref={inputRef}
value={editValue}
onChange={e => setEditValue(e.target.value)}
onBlur={finishEdit}
onKeyDown={e => {
if (e.key === "Enter") finishEdit();
if (e.key === "Escape") {
setEditValue(value ?? "");
setEditing(false);
}
}}
style={{
width: "100%",
background: "transparent",
border: "none",
outline: "none",
font: "inherit",
padding: 0,
margin: 0,
textAlign: align || "left",
}}
/>
) : (
<span style={{ display: "block", width: "100%", overflow: "hidden", textOverflow: "ellipsis" }}>
{colKey === "ccllc_jobnumber" ? (value ?? "").replace(/,/g, "") : value}
</span>
)}
</td>
);
}
/* ─────────────────────────── EditableHeaderCell ─────────────────────────── */
function EditableHeaderCell({
value,
onSave,
colKey,
isSticky,
width,
isFirstNonSticky,
align,
}: {
value: string;
onSave: (newValue: string) => void;
colKey: string;
isSticky?: boolean;
width?: number;
isFirstNonSticky?: boolean;
align?: "left" | "center" | "right";
}) {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editing]);
useEffect(() => setEditValue(value), [value]);
const handleCellClick = (e: React.MouseEvent) => {
if (!(e.target as HTMLElement).closest(".header-dropdown-btn")) {
setEditing(true);
}
};
const handleSave = () => {
if (editValue !== value) onSave(editValue);
setEditing(false);
};
const handleInputBlur = () => handleSave();
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") handleSave();
else if (e.key === "Escape") {
setEditValue(value);
setEditing(false);
}
};
// Sticky header cells for "Job Number" and "Job Name" should have top: 0 to stay fixed on vertical scroll
const isStickyHeader = isSticky && (colKey === "ccllc_jobnumber" || colKey === "ccllc_jobname");
// Always sticky at top for all header cells
const headerStyle: React.CSSProperties = {
minWidth: width ? `${width}px` : "60px",
maxWidth: width ? `${width + 20}px` : undefined,
width: width ? `${width}px` : undefined,
borderTop: "1px solid #000",
borderLeft: isFirstNonSticky ? "none" : "1px solid #000",
borderRight: value === "Job Number" ? "none" : "1px solid #000",
borderBottom: "1px solid #000",
boxSizing: "border-box",
position: "sticky",
top: 0,
left: isSticky && colKey === "ccllc_jobnumber" ? 0 : isSticky && colKey === "ccllc_jobname" ? 100 : undefined,
zIndex: isSticky
? 2000 // sticky columns in header
: 1500, // normal header
background: "#f3f3f3",
whiteSpace: "normal",
padding: 0,
fontWeight: 600,
textAlign: align || "left",
cursor: "pointer",
userSelect: "none",
minHeight: "40px",
height: "40px",
overflow: "visible",
};
const flexRowStyle: React.CSSProperties = {
display: "flex",
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
width: "100%",
height: "100%",
minHeight: "40px",
overflow: "visible",
};
const labelAreaStyle: React.CSSProperties = {
flex: "1 1 0",
minWidth: 0,
display: "flex",
alignItems: "center",
padding: "8px 8px 8px 10px",
boxSizing: "border-box",
overflow: "hidden",
};
const dropdownCellStyle: React.CSSProperties = {
width: "20px",
minWidth: "20px",
maxWidth: "20px",
height: "40px",
boxSizing: "border-box",
overflow: "visible",
display: "flex",
alignItems: "flex-end",
justifyContent: "center",
paddingBottom: "4px",
zIndex: 1600,
};
const dropdownButtonStyle: React.CSSProperties = {
width: "16px",
height: "16px",
minWidth: "16px",
minHeight: "16px",
padding: 0,
margin: 0,
borderRadius: "4px",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#C0C0C0",
zIndex: 1600,
};
const renderHeaderText = (text: string) => {
const lines = text.split("\n");
return (
<span style={{ display: "block", width: "100%", wordBreak: "break-word", whiteSpace: "pre-line" }}>
{lines.map((line, idx) => (
<React.Fragment key={idx}>
{line}
{idx < lines.length - 1 ? <br /> : null}
</React.Fragment>
))}
</span>
);
};
return (
<th style={headerStyle} onClick={handleCellClick} tabIndex={0} aria-label={`Edit column: ${value}`}>
<div style={flexRowStyle}>
<div style={labelAreaStyle}>
{editing ? (
<input
ref={inputRef}
value={editValue}
onChange={e => setEditValue(e.target.value)}
onBlur={handleInputBlur}
onKeyDown={handleInputKeyDown}
style={{
width: "100%",
background: "transparent",
border: "none",
outline: "none",
font: "inherit",
padding: 0,
margin: 0,
color: "#222",
whiteSpace: "pre-line",
textAlign: align || "left",
}}
/>
) : (
renderHeaderText(editValue)
)}
</div>
<div style={dropdownCellStyle}>
<Menu>
<MenuTrigger disableButtonEnhancement>
<Button
appearance="subtle"
size="small"
icon={<ChevronDownRegular fontSize="15px" />}
aria-label="Column options"
className="header-dropdown-btn"
style={dropdownButtonStyle}
tabIndex={0}
/>
</MenuTrigger>
<MenuPopover style={{ zIndex: 1700 }}>
<MenuList>
<MenuItem>Column options</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
</div>
</div>
</th>
);
}
/* ─────────────────────────────── MAIN COMPONENT ─────────────────────────────── */
const BATCH_SIZE = 50; // Show 50 records at a time
const DEFAULT_SEARCH_BATCH_SIZE = 10;
const SEARCH_ALL_PAGE_SIZE = 100;
const GeneratedComponent: React.FC<GeneratedComponentProps> = ({ dataApi }) => {
const [mainRows, setMainRows] = useState<ReadableTableRow<ccllc_job>[]>([]);
const [searchRows, setSearchRows] = useState<ReadableTableRow<ccllc_job>[]>([]);
const [searchText, setSearchText] = useState("");
const [searchAll, setSearchAll] = useState(false);
const [loading, setLoading] = useState(true);
const [searchLoading, setSearchLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [headerNames, setHeaderNames] = useState({ ...DEFAULT_FRIENDLY_NAMES });
const [deleting, setDeleting] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [rowToDelete, setRowToDelete] = useState<ReadableTableRow<ccllc_job> | null>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
let mounted = true;
const load = async () => {
try {
setLoading(true);
const { rows } = await dataApi.queryTable("ccllc_job", {
select: DEFAULT_COLUMN_ORDER,
pageSize: BATCH_SIZE,
orderBy: "ccllc_jobnumber desc",
});
const clean = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
if (mounted) setMainRows(clean);
} catch {
if (mounted) setError("Failed to load jobs.");
} finally {
if (mounted) setLoading(false);
}
};
load();
return () => { mounted = false; };
}, [dataApi]);
useEffect(() => {
if (!searchText.trim()) {
setSearchRows([]);
setSearchLoading(false);
return;
}
let cancelled = false;
const run = async () => {
setSearchLoading(true);
const cols = [
"ccllc_jobname", "ccllc_jobnumber", "ccllc_jobaddress", "ccllc_clientcompany",
"ccllc_contactperson", "ccllc_contactemail", "ccllc_notes", "ccllc_estimatorname",
"ccllc_officerepresentative",
];
const filter = cols
.map(c => `contains(${c},'${searchText.replace(/'/g, "''")}')`)
.join(" or ");
const allResults: ReadableTableRow<ccllc_job>[] = [];
const pageSize = searchAll ? 100 : DEFAULT_SEARCH_BATCH_SIZE; // 100 or 10
let skip = 0;
let hasMore = searchAll; // Only paginate if "Show All"
try {
// Initial request
let opts: QueryTableOptions<ccllc_job> = {
select: DEFAULT_COLUMN_ORDER,
pageSize,
orderBy: "ccllc_jobnumber desc",
filter,
skip,
};
let { rows, nextLink } = await dataApi.queryTable("ccllc_job", opts);
// Clean and store first page
const clean = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
allResults.push(...clean);
// Only continue if "Show All" and there's more
while (!cancelled && searchAll && nextLink && clean.length === pageSize) {
skip += pageSize;
opts = { ...opts, skip };
const result = await dataApi.queryTable("ccllc_job", opts);
rows = result.rows;
nextLink = result.nextLink;
const cleanPage = rows.map(r => {
const { ["@Odata.Etag"]: _, ...rest } = r;
return rest;
});
allResults.push(...cleanPage);
}
if (!cancelled) setSearchRows(allResults);
} catch (err) {
if (!cancelled) setError("Search failed.");
} finally {
if (!cancelled) setSearchLoading(false);
}
};
run();
return () => { cancelled = true; };
}, [searchText, searchAll, dataApi]);
const handleCellSave = async (rowId: string, colKey: string, newValue: string) => {
try {
const all = [...searchRows, ...mainRows];
const row = all.find(r => r.ccllc_jobid === rowId);
if (!row) return;
const final = colKey === "ccllc_jobnumber" ? newValue.replace(/,/g, "") : newValue;
if ((row as any)[colKey] === final) return;
await dataApi.updateRow("ccllc_job", rowId, { [colKey]: final });
const update = (list: typeof all) =>
list.map(r => (r.ccllc_jobid === rowId ? { ...r, [colKey]: final } : r));
setSearchRows(update);
setMainRows(update);
} catch {
setError("Update failed.");
}
};
const handleHeaderSave = (col: string, name: string) => {
setHeaderNames(p => ({ ...p, [col]: name }));
};
const openDelete = (row: ReadableTableRow<ccllc_job>) => {
setRowToDelete(row);
setDeleteDialogOpen(true);
};
const confirmDelete = async () => {
if (!rowToDelete) return;
setDeleting(true);
try {
await dataApi.deleteRow("ccllc_job", rowToDelete.ccllc_jobid);
const id = rowToDelete.ccllc_jobid;
setSearchRows(p => p.filter(r => r.ccllc_jobid !== id));
setMainRows(p => p.filter(r => r.ccllc_jobid !== id));
} catch {
setError("Delete failed.");
} finally {
setDeleting(false);
setDeleteDialogOpen(false);
setRowToDelete(null);
}
};
const visibleCols = (mainRows.length
? getJobColumns(mainRows[0])
: DEFAULT_COLUMN_ORDER
).filter(c => c !== "@Odata.Etag" && DEFAULT_COLUMN_VISIBILITY[c] !== false);
const firstNonStickyIdx = visibleCols.findIndex(
c => c !== "ccllc_jobnumber" && c !== "ccllc_jobname"
);
const rows = searchText.trim() ? searchRows : mainRows;
const isShowAllSearch = searchText.trim() && searchAll;
const displayedRows = isShowAllSearch ? rows : rows.slice(0, 50);
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
boxSizing: "border-box",
padding: 20,
}}
>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 12, gap: 12, flexShrink: 0, minHeight: 36 }}>
<div style={{ border: "2px solid #000", background: "#f3f3f3", borderRadius: 8, padding: "0px 16px",display: "flex", height: 36, }}>
<Text as="h1" size={700} weight="semibold">Job List</Text>
</div>
<div style={{ display: "flex", gap: 8 }}>
<Input
value={searchText}
onChange={(_, d) => setSearchText(d.value)}
placeholder="Search jobs..."
contentBefore={<SearchRegular />}
style={{ height: 36 }}
/>
<Button
appearance="subtle"
onClick={() => setSearchAll(a => !a)}
style={{ background: searchAll ? "#e5ffe5" : "#f3f3f3", border: "1px solid #000", }}
>
{searchAll ? "Top 10" : "Show All"}
</Button>
</div>
</div>
{error && <Text style={{ color: "#b00", marginBottom: 8, flexShrink: 0 }}>{error}</Text>}
{/* TABLE CONTAINER */}
<div
style={{
flex: 1,
minHeight: 0,
border: "1px solid #000",
borderRadius: 6,
overflow: "hidden",
position: "relative",
display: "flex",
flexDirection: "column",
}}
>
{(loading || searchLoading) && displayedRows.length === 0 ? (
<div style={{ flex: 1, display: "flex", justifyContent: "center", alignItems: "center" }}>
<Spinner size="medium" label="Loading…" />
</div>
) : (
<div
ref={scrollRef}
style={{
flex: 1,
minHeight: 0,
maxHeight: 600, // Force vertical scroll for more than 50 rows
overflowY: "auto",
overflowX: "auto",
WebkitOverflowScrolling: "touch",
boxSizing: "border-box",
}}
>
<table
style={{
borderCollapse: "separate",
borderSpacing: 0,
width: "max-content",
minWidth: "100%",
tableLayout: "fixed",
}}
>
<thead>
<tr>
{visibleCols.map((col, idx) => (
<EditableHeaderCell
key={col}
colKey={col}
value={headerNames[col] || humanize(col)}
onSave={v => handleHeaderSave(col, v)}
isSticky={col === "ccllc_jobnumber" || col === "ccllc_jobname"}
width={DEFAULT_COLUMN_WIDTHS[col]}
isFirstNonSticky={idx === firstNonStickyIdx}
align={DEFAULT_COLUMN_ALIGNMENTS[col]}
/>
))}
<th
style={{
minWidth: 60,
width: 60,
border: "1px solid #000",
background: "#f3f3f3",
position: "sticky",
top: 0,
right: undefined,
zIndex: 1500,
transform: "none",
}}
>
Delete
</th>
</tr>
</thead>
<tbody>
{displayedRows.map(row => (
<tr key={row.ccllc_jobid}>
{visibleCols.map((col, idx) => (
<EditableCell
key={col}
value={row[col as keyof typeof row]?.toString() ?? ""}
onSave={v => handleCellSave(row.ccllc_jobid, col, v)}
rowId={row.ccllc_jobid}
colKey={col}
isSticky={col === "ccllc_jobnumber" || col === "ccllc_jobname"}
width={DEFAULT_COLUMN_WIDTHS[col]}
disabled={col === "ccllc_jobid"}
isFirstNonSticky={idx === firstNonStickyIdx}
align={DEFAULT_COLUMN_ALIGNMENTS[col]}
/>
))}
<td
style={{
minWidth: 60,
width: 60,
borderTop: "1px solid #000",
borderBottom: "1px solid #000",
borderLeft: "1px solid #000",
background: "#fff",
position: "static",
right: undefined,
zIndex: undefined,
transform: "none",
textAlign: "center",
}}
>
<Button
appearance="subtle"
icon={<DeleteRegular />}
onClick={() => openDelete(row)}
disabled={deleting}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Delete Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={(_, { open }) => !open && setDeleteDialogOpen(false)}>
<DialogSurface>
<DialogBody>
<DialogTitle>Confirm Delete</DialogTitle>
<Text block size={400} style={{ marginBottom: 16 }}>
Are you sure you want to delete this job record?
</Text>
<DialogActions>
<Button appearance="secondary" onClick={() => setDeleteDialogOpen(false)} disabled={deleting}>
Cancel
</Button>
<Button
appearance="primary"
style={{ backgroundColor: "#b00", color: "#fff" }}
onClick={confirmDelete}
disabled={deleting}
>
{deleting ? "Deleting…" : "Delete"}
</Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</Dialog>
</div>
);
};
export const TablePage = GeneratedComponent;
export default TablePage;
+23
View File
@@ -0,0 +1,23 @@
// table.tsx
import { Table, TableBody, TableCell, TableRow } from '@fluentui/react-components';
const SimpleTable = () => (
<Table>
<TableBody>
<TableRow>
<TableCell><strong>Name</strong></TableCell>
<TableCell><strong>Age</strong></TableCell>
</TableRow>
<TableRow>
<TableCell>Alice</TableCell>
<TableCell>30</TableCell>
</TableRow>
<TableRow>
<TableCell>Bob</TableCell>
<TableCell>25</TableCell>
</TableRow>
</TableBody>
</Table>
);
export default SimpleTable;
+13
View File
@@ -0,0 +1,13 @@
export const DEBUG = true; // turn off for production
export function log(...args) {
if (DEBUG) console.log("[MSAL DEBUG]", ...args);
}
export function warn(...args) {
if (DEBUG) console.warn("[MSAL WARN]", ...args);
}
export function error(...args) {
if (DEBUG) console.error("[MSAL ERROR]", ...args);
}
+21
View File
@@ -0,0 +1,21 @@
import { PublicClientApplication } from "@azure/msal-browser";
export const msalConfig = {
auth: {
clientId: "3c846e71-9609-40e1-b458-0eb805e21b9f",
authority: "https://login.microsoftonline.com/3fd97ea7-b124-41f1-855f-52d8ac3b16c7",
redirectUri: "http://localhost:3000/auth-response",
postLogoutRedirectUri: "http://localhost:3000/auth-response"
},
cache: {
cacheLocation: "localStorage",
storeAuthStateInCookie: true
}
};
export const loginRequest = {
scopes: ["User.Read"]
};
log("Initializing MSAL...");
export const msalInstance = new PublicClientApplication(msalConfig);
log("MSAL instance created:", msalInstance);
+13
View File
@@ -0,0 +1,13 @@
import app from './pb';
const PORT = 8081;
console.log(`✅ Starting Impersonation server at http://localhost:${PORT}`);
Bun.serve({
port: PORT,
fetch: app.fetch,
});
console.log(`✅ Impersonation server running at http://localhost:${PORT}`);
console.log(`Press Ctrl+C to stop`);
+91
View File
@@ -0,0 +1,91 @@
import { Hono } from 'hono';
import PocketBase from 'pocketbase';
const app = new Hono();
// --- PocketBase singleton ---
const pb = new PocketBase(process.env.PB_URL || 'http://10.10.1.47:8080');
// --- User type ---
export type PBUser = {
id: string;
role: 'Admin' | 'Superuser' | string;
display_name?: string;
email?: string;
};
// --- Middleware: require Admin or Superuser ---
async function requireAdmin(c: any, next: () => Promise<void>) {
const authHeader = c.req.header('authorization')?.replace('Bearer ', '');
if (!authHeader) return c.text('Unauthorized', 401);
pb.authStore.save(authHeader, null);
const currentUser = pb.authStore.record as PBUser | null;
if (!currentUser) return c.text('Unauthorized', 401);
if (currentUser.role !== 'Admin' && currentUser.role !== 'Superuser') {
return c.text('Forbidden', 403);
}
c.set('currentUser', currentUser);
await next();
}
// --- Impersonation Endpoint ---
app.post('/api/impersonate', requireAdmin, async (c: any) => {
const actor = c.get('currentUser') as PBUser;
if (!actor) return c.text('Unauthorized', 401);
const { targetUserIdentifier } = await c.req.json(); // can be ID or email
let targetUser: PBUser | null = null;
// Try fetching by ID first
try {
targetUser = await pb.collection('users').getOne(targetUserIdentifier) as PBUser;
} catch {
// If failed, try fetching by email
const users = await pb.collection('users').getFullList({ sort: '-created' });
const foundUser = users.find(u => u.email === targetUserIdentifier);
if (foundUser) {
targetUser = {
id: foundUser.id,
role: foundUser.role,
display_name: foundUser.display_name,
email: foundUser.email
};
} else {
targetUser = null;
}
}
if (!targetUser) return c.text('Target user not found', 404);
// Prevent Admin → Admin impersonation
if (actor.role === 'Admin' && targetUser.role === 'Admin') {
return c.text('Admins cannot impersonate other admins', 403);
}
// Log impersonation
await pb.collection('admin_audit_logs').create({
admin_id: actor.id,
admin_type: actor.role === 'Superuser' ? 'superuser' : 'user',
target_user_id: targetUser.id,
action: 'IMPERSONATE_USER',
level: actor.role === 'Superuser' ? 'SUPERUSER' : 'ADMIN',
created_at: new Date().toISOString(),
target_display_name: targetUser.display_name || null,
actor_display_name: actor.display_name || null,
});
// Issue impersonation token
const tokenResponse = await pb.send('/api/collections/users/auth-refresh', { method: 'POST' });
const json = await tokenResponse.json();
return c.json({ token: json.token });
});
export default app;
export { pb };
+88
View File
@@ -0,0 +1,88 @@
import { Hono } from 'hono';
import { serve } from 'bun';
import { serveStatic } from 'hono/bun';
const app = new Hono();
app.get("/table", (c) => {
return c.html(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Power Apps Table</title>
<link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.1.0/css/fabric.min.css" />
<style>
body, html, #root { margin:0; padding:0; height:100%; font-family:'Segoe UI',sans-serif; }
#root { padding:2rem; overflow:auto; }
</style>
</head>
<body>
<div id="root"></div>
<!-- LOAD REACT GLOBALLY -->
<script type="module">
// Load React from CDN into window
window.React = (await import("https://esm.sh/react@18")).default;
window.ReactDOM = (await import("https://esm.sh/react-dom@18")).default;
console.log("React loaded:", window.React);
// Now load your component
import("./table.js").then(module => {
const TablePage = module.default;
console.log("TablePage:", TablePage);
ReactDOM.createRoot(document.getElementById("root"))
.render(React.createElement(TablePage));
}).catch(err => {
console.error("Failed to load table.js:", err);
});
</script>
</body>
</html>
`);
});
// === CONFIG: Load from environment variables ===
const API_URL = process.env.API_URL!;
const API_KEY = process.env.API_KEY!;
const port = Number(process.env.PORT || 3000);
// Serve static assets from /public
//app.use('*', serveStatic({ root: './public' }));
// Default route serves index.html (handled by static middleware); fallback text if not found
// {made this text to text react table} app.get('/', (c) => c.redirect('/index.html'));
//serve({
// port,
// fetch: app.fetch,
//});
//console.log(`Server running on http://localhost:${port}`);
// 2. SERVE ONLY STATIC FILES (NOT ROUTES)
app.use("/table.js", serveStatic({ path: "./public/table.js" })); // serve JS
app.use("/favicon.ico", serveStatic({ path: "./public/favicon.ico" })); // optional
// 3. FALLBACK: serve index.html for home (optional)
app.get("/", (c) => {
return c.html(`
<!DOCTYPE html>
<html><head><title>Home</title></head>
<body style="font-family:Segoe UI;text-align:center;padding:4rem;">
<h1>Power Apps Table</h1>
<p><a href="/table">Open Table</a></p>
</body></html>
`);
});
export default {
port: 3000,
fetch: app.fetch,
};
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}