INIT frontend
This commit is contained in:
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[dev]
|
||||||
|
port = 3000
|
||||||
|
static = "./public"
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
console.log("Hello via Bun!");
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
* © 2015–2025 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
|
||||||
|
* © 2015–2025 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,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>
|
||||||
@@ -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 <script> 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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('&','&').replaceAll('<','<').replaceAll('>','>'); }
|
||||||
|
|
||||||
|
// --- 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('&','&').replaceAll('<','<').replaceAll('>','>'); }
|
||||||
|
|
||||||
|
/* 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
@@ -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>
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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`);
|
||||||
@@ -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 };
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user