mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 06:33:12 +00:00
chore(form): add template JavaScript checks (#92)
* fix(auth): require Google authentication in cloud mode Remove the public shared-key mechanism and fail closed on Google auth while adding submit-route throttling. * chore(form): lint template JavaScript in CI * docs(form): record frontend delivery decisions * fix: resolve remaining merge conflicts
This commit is contained in:
parent
311eab35c0
commit
bcf255b17f
8 changed files with 1946 additions and 796 deletions
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
|
@ -12,6 +12,25 @@ jobs:
|
|||
with:
|
||||
run-tests: false
|
||||
|
||||
template-js:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.x
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- name: Install JavaScript tooling
|
||||
run: npm ci
|
||||
|
||||
- name: Check template JavaScript
|
||||
run: npm run check:templates
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ output/
|
|||
.DS_Store
|
||||
.aws-sam/
|
||||
samconfig.toml
|
||||
node_modules/
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ reminders to employees └──────
|
|||
who haven't ordered
|
||||
```
|
||||
|
||||
### Form frontend decisions
|
||||
|
||||
- **Theme:** Retain the current order-form palette and typography. There is no shared Sea Haven web design system to adopt, and changing the theme without one would be an isolated visual redesign. Future theme changes should remap the existing CSS custom properties instead of adding scattered color values or inline styles.
|
||||
- **Deployment:** Keep generating and publishing one self-contained HTML file. Inlining the template CSS and JavaScript at build time preserves the current atomic S3 upload and avoids introducing asset versioning, cache coordination, and additional CloudFront invalidation paths. Revisit a multi-file deploy only when asset size, cross-page sharing, or independent caching provides a concrete benefit.
|
||||
- **Visual regression:** Do not add visual-regression CI now. Structural and Playwright coverage remain the active safeguards. Revisit snapshots, computed-style assertions, or a hosted service only after a layout or token regression reaches production without those checks catching it.
|
||||
|
||||
ESLint and Prettier check the JavaScript template sources in CI. They do not lint
|
||||
the generated HTML, and the generated deployment artifact remains self-contained.
|
||||
|
||||
## Weekly Flow
|
||||
|
||||
| When | What | How |
|
||||
|
|
|
|||
20
eslint.config.mjs
Normal file
20
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import eslint from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{
|
||||
files: ['src/server/templates/*.js'],
|
||||
...eslint.configs.recommended,
|
||||
languageOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'script',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
google: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
},
|
||||
},
|
||||
];
|
||||
934
package-lock.json
generated
Normal file
934
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,934 @@
|
|||
{
|
||||
"name": "meal-order-manager",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meal-order-manager",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"eslint": "10.8.0",
|
||||
"globals": "17.8.0",
|
||||
"prettier": "3.9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
||||
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/regexpp": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
||||
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
|
||||
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^3.0.5",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^10.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
|
||||
"integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
|
||||
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
|
||||
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
|
||||
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
|
||||
"integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.2.1",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/types": "^0.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/node": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
||||
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@humanfs/core": "^0.19.2",
|
||||
"@humanfs/types": "^0.15.0",
|
||||
"@humanwhocodes/retry": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/types": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
|
||||
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/module-importer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
||||
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/retry": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
||||
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/esrecurse": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-jsx": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
|
||||
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@eslint/config-array": "^0.23.5",
|
||||
"@eslint/config-helpers": "^0.7.0",
|
||||
"@eslint/core": "^1.2.1",
|
||||
"@eslint/plugin-kit": "^0.7.2",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.14.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"eslint-scope": "^9.1.2",
|
||||
"eslint-visitor-keys": "^5.0.1",
|
||||
"espree": "^11.2.0",
|
||||
"esquery": "^1.7.0",
|
||||
"esutils": "^2.0.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^8.0.0",
|
||||
"find-up": "^5.0.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"ignore": "^5.2.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"minimatch": "^10.2.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
"bin": {
|
||||
"eslint": "bin/eslint.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"jiti": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"jiti": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
||||
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@types/esrecurse": "^4.3.1",
|
||||
"@types/estree": "^1.0.8",
|
||||
"esrecurse": "^4.3.0",
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/espree": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
||||
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.16.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/esquery": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
||||
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/esrecurse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"estraverse": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
||||
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-json-stable-stringify": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flat-cache": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^6.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"flatted": "^3.2.9",
|
||||
"keyv": "^4.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
|
||||
"integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.8.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
|
||||
"integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "~0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"deep-is": "^0.1.3",
|
||||
"fast-levenshtein": "^2.0.6",
|
||||
"levn": "^0.4.1",
|
||||
"prelude-ls": "^1.2.1",
|
||||
"type-check": "^0.4.0",
|
||||
"word-wrap": "^1.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"yocto-queue": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
||||
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.6",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prelude-ls": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
package.json
Normal file
17
package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "meal-order-manager",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"check:templates": "npm run lint:templates && npm run format:templates",
|
||||
"format:templates": "prettier --check \"src/server/templates/*.js\"",
|
||||
"format:templates:write": "prettier --write \"src/server/templates/*.js\"",
|
||||
"lint:templates": "eslint \"src/server/templates/*.js\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
"eslint": "10.8.0",
|
||||
"globals": "17.8.0",
|
||||
"prettier": "3.9.6"
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ const MEALS = window.MEALS;
|
|||
const WEEK = window.WEEK;
|
||||
const ADMIN_REQUEST_TIMEOUT_MS = 10000;
|
||||
|
||||
let currentAdminWeek = '';
|
||||
let currentAdminWeek = "";
|
||||
let editingOrder = null;
|
||||
let editQuantities = {};
|
||||
let lastAdminData = null;
|
||||
|
|
@ -15,19 +15,25 @@ let latestAdminRequest = 0;
|
|||
function adminHeaders() {
|
||||
const h = {};
|
||||
const cred = window.googleCredential;
|
||||
if (cred) h['Authorization'] = 'Bearer ' + cred;
|
||||
if (cred) h["Authorization"] = "Bearer " + cred;
|
||||
return h;
|
||||
}
|
||||
|
||||
function adminFetch(url, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), ADMIN_REQUEST_TIMEOUT_MS);
|
||||
return fetch(url, { ...options, signal: controller.signal })
|
||||
.finally(() => clearTimeout(timeout));
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
ADMIN_REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
return fetch(url, { ...options, signal: controller.signal }).finally(() =>
|
||||
clearTimeout(timeout),
|
||||
);
|
||||
}
|
||||
|
||||
function setAdminActionDisabled(action, disabled) {
|
||||
document.querySelectorAll('[data-admin-action="' + action + '"]').forEach(btn => {
|
||||
document
|
||||
.querySelectorAll('[data-admin-action="' + action + '"]')
|
||||
.forEach((btn) => {
|
||||
btn.disabled = disabled;
|
||||
});
|
||||
}
|
||||
|
|
@ -35,34 +41,38 @@ function setAdminActionDisabled(action, disabled) {
|
|||
function loadWeekOrders(week) {
|
||||
if (!week) return;
|
||||
const requestId = ++latestAdminRequest;
|
||||
window.FormUtils.clearStatus('admin');
|
||||
window.FormUtils.clearStatus("admin");
|
||||
currentAdminWeek = week;
|
||||
editingOrder = null;
|
||||
editQuantities = {};
|
||||
const tbody = document.getElementById('admin-tbody');
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="admin-loading">Loading...</td></tr>';
|
||||
const cards = document.getElementById('admin-cards');
|
||||
const tbody = document.getElementById("admin-tbody");
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="5" class="admin-loading">Loading...</td></tr>';
|
||||
const cards = document.getElementById("admin-cards");
|
||||
cards.innerHTML = '<div class="admin-loading">Loading...</div>';
|
||||
document.getElementById('admin-empty').classList.add('is-hidden');
|
||||
document.getElementById('admin-stats').textContent = '';
|
||||
document.getElementById("admin-empty").classList.add("is-hidden");
|
||||
document.getElementById("admin-stats").textContent = "";
|
||||
|
||||
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
adminFetch(ADMIN_URL + "?week=" + encodeURIComponent(week), {
|
||||
headers: adminHeaders(),
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
if (requestId === latestAdminRequest && week === currentAdminWeek) {
|
||||
renderAdminTable(data);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId === latestAdminRequest && week === currentAdminWeek) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="admin-error">Failed to load orders.</td></tr>';
|
||||
cards.innerHTML = '';
|
||||
window.FormUtils.showStatus('Failed to load orders.', {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
tbody.innerHTML =
|
||||
'<tr><td colspan="5" class="admin-error">Failed to load orders.</td></tr>';
|
||||
cards.innerHTML = "";
|
||||
window.FormUtils.showStatus("Failed to load orders.", {
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -71,52 +81,52 @@ function loadWeekOrders(week) {
|
|||
window.loadWeekOrders = loadWeekOrders;
|
||||
|
||||
function buildEditQtyRow(name, qty) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = qty === 0 ? 'edit-qty removed' : 'edit-qty';
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = qty === 0 ? "edit-qty removed" : "edit-qty";
|
||||
|
||||
const minusBtn = document.createElement('button');
|
||||
minusBtn.type = 'button';
|
||||
minusBtn.dataset.adminAction = 'edit-qty';
|
||||
const minusBtn = document.createElement("button");
|
||||
minusBtn.type = "button";
|
||||
minusBtn.dataset.adminAction = "edit-qty";
|
||||
minusBtn.dataset.mealName = name;
|
||||
minusBtn.dataset.delta = '-1';
|
||||
minusBtn.setAttribute('aria-label', 'Remove one ' + name);
|
||||
minusBtn.innerHTML = '−';
|
||||
minusBtn.dataset.delta = "-1";
|
||||
minusBtn.setAttribute("aria-label", "Remove one " + name);
|
||||
minusBtn.innerHTML = "−";
|
||||
|
||||
const span = document.createElement('span');
|
||||
const span = document.createElement("span");
|
||||
span.textContent = qty;
|
||||
span.setAttribute('aria-label', 'Quantity of ' + name);
|
||||
span.setAttribute("aria-label", "Quantity of " + name);
|
||||
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.type = 'button';
|
||||
plusBtn.dataset.adminAction = 'edit-qty';
|
||||
const plusBtn = document.createElement("button");
|
||||
plusBtn.type = "button";
|
||||
plusBtn.dataset.adminAction = "edit-qty";
|
||||
plusBtn.dataset.mealName = name;
|
||||
plusBtn.dataset.delta = '1';
|
||||
plusBtn.setAttribute('aria-label', 'Add one ' + name);
|
||||
plusBtn.textContent = '+';
|
||||
plusBtn.dataset.delta = "1";
|
||||
plusBtn.setAttribute("aria-label", "Add one " + name);
|
||||
plusBtn.textContent = "+";
|
||||
|
||||
wrap.appendChild(minusBtn);
|
||||
wrap.appendChild(span);
|
||||
wrap.appendChild(plusBtn);
|
||||
wrap.appendChild(document.createTextNode(' ' + name));
|
||||
wrap.appendChild(document.createTextNode(" " + name));
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function buildAddItemSelect(availableMeals) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'admin-add-item';
|
||||
const select = document.createElement('select');
|
||||
select.dataset.adminAction = 'add-item-select';
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "admin-add-item";
|
||||
const select = document.createElement("select");
|
||||
select.dataset.adminAction = "add-item-select";
|
||||
|
||||
const defaultOpt = document.createElement('option');
|
||||
defaultOpt.value = '';
|
||||
defaultOpt.textContent = '+ Add item...';
|
||||
const defaultOpt = document.createElement("option");
|
||||
defaultOpt.value = "";
|
||||
defaultOpt.textContent = "+ Add item...";
|
||||
select.appendChild(defaultOpt);
|
||||
|
||||
availableMeals.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
availableMeals.forEach((m) => {
|
||||
const opt = document.createElement("option");
|
||||
const price = m.price || 0;
|
||||
opt.value = m.name;
|
||||
opt.textContent = m.name + ' ($' + price.toFixed(2) + ')';
|
||||
opt.textContent = m.name + " ($" + price.toFixed(2) + ")";
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
|
|
@ -125,8 +135,8 @@ function buildAddItemSelect(availableMeals) {
|
|||
}
|
||||
|
||||
function buildActionButton(label, action, extra) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.dataset.adminAction = action;
|
||||
if (extra) {
|
||||
Object.entries(extra).forEach(([key, val]) => {
|
||||
|
|
@ -138,24 +148,24 @@ function buildActionButton(label, action, extra) {
|
|||
}
|
||||
|
||||
function buildEditActions(email) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'admin-actions';
|
||||
const saveBtn = buildActionButton('Save', 'save-edit', { email });
|
||||
saveBtn.className = 'btn btn-sm btn-save';
|
||||
const cancelBtn = buildActionButton('Cancel', 'cancel-edit');
|
||||
cancelBtn.className = 'btn btn-sm btn-cancel';
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "admin-actions";
|
||||
const saveBtn = buildActionButton("Save", "save-edit", { email });
|
||||
saveBtn.className = "btn btn-sm btn-save";
|
||||
const cancelBtn = buildActionButton("Cancel", "cancel-edit");
|
||||
cancelBtn.className = "btn btn-sm btn-cancel";
|
||||
wrap.appendChild(saveBtn);
|
||||
wrap.appendChild(cancelBtn);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function buildViewActions(email, name) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'admin-actions';
|
||||
const editBtn = buildActionButton('Edit', 'start-edit', { email });
|
||||
editBtn.className = 'btn btn-sm btn-edit';
|
||||
const deleteBtn = buildActionButton('Delete', 'delete', { email, name });
|
||||
deleteBtn.className = 'btn btn-sm btn-delete';
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "admin-actions";
|
||||
const editBtn = buildActionButton("Edit", "start-edit", { email });
|
||||
editBtn.className = "btn btn-sm btn-edit";
|
||||
const deleteBtn = buildActionButton("Delete", "delete", { email, name });
|
||||
deleteBtn.className = "btn btn-sm btn-delete";
|
||||
wrap.appendChild(editBtn);
|
||||
wrap.appendChild(deleteBtn);
|
||||
return wrap;
|
||||
|
|
@ -163,90 +173,97 @@ function buildViewActions(email, name) {
|
|||
|
||||
function formatSubmittedAt(submittedAt) {
|
||||
return submittedAt
|
||||
? new Date(submittedAt).toLocaleString('en-US', {
|
||||
timeZone: 'America/New_York',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
? new Date(submittedAt).toLocaleString("en-US", {
|
||||
timeZone: "America/New_York",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: '';
|
||||
: "";
|
||||
}
|
||||
|
||||
function editTotal() {
|
||||
const menuPrices = {};
|
||||
MEALS.forEach(m => { menuPrices[m.name] = m.price; });
|
||||
MEALS.forEach((m) => {
|
||||
menuPrices[m.name] = m.price;
|
||||
});
|
||||
return Object.keys(editQuantities).reduce((sum, name) => {
|
||||
const price = menuPrices[name] || 0;
|
||||
return sum + window.FormUtils.employeePrice(price) * (editQuantities[name] || 0);
|
||||
return (
|
||||
sum +
|
||||
window.FormUtils.employeePrice(price) * (editQuantities[name] || 0)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function populateOrderItems(container, order, isEditing, separatorFactory) {
|
||||
if (isEditing) {
|
||||
Object.keys(editQuantities).forEach(name => {
|
||||
Object.keys(editQuantities).forEach((name) => {
|
||||
container.appendChild(buildEditQtyRow(name, editQuantities[name] || 0));
|
||||
if (separatorFactory) container.appendChild(separatorFactory());
|
||||
});
|
||||
const availableMeals = MEALS.filter(
|
||||
m => !Object.prototype.hasOwnProperty.call(editQuantities, m.name)
|
||||
(m) => !Object.prototype.hasOwnProperty.call(editQuantities, m.name),
|
||||
);
|
||||
if (availableMeals.length > 0) {
|
||||
container.appendChild(buildAddItemSelect(availableMeals));
|
||||
}
|
||||
return '$' + editTotal().toFixed(2);
|
||||
return "$" + editTotal().toFixed(2);
|
||||
}
|
||||
|
||||
order.items.forEach((item, itemIdx) => {
|
||||
if (separatorFactory && itemIdx > 0) {
|
||||
container.appendChild(separatorFactory());
|
||||
}
|
||||
const row = separatorFactory ? container : document.createElement('div');
|
||||
if (!separatorFactory) row.className = 'admin-item';
|
||||
row.appendChild(document.createTextNode(item.name + ' ×' + item.quantity + ' '));
|
||||
const sub = document.createElement('span');
|
||||
sub.className = 'admin-muted';
|
||||
sub.textContent = '($' + item.subtotal.toFixed(2) + ')';
|
||||
const row = separatorFactory ? container : document.createElement("div");
|
||||
if (!separatorFactory) row.className = "admin-item";
|
||||
row.appendChild(
|
||||
document.createTextNode(item.name + " ×" + item.quantity + " "),
|
||||
);
|
||||
const sub = document.createElement("span");
|
||||
sub.className = "admin-muted";
|
||||
sub.textContent = "($" + item.subtotal.toFixed(2) + ")";
|
||||
row.appendChild(sub);
|
||||
if (!separatorFactory) container.appendChild(row);
|
||||
});
|
||||
return '$' + order.total.toFixed(2);
|
||||
return "$" + order.total.toFixed(2);
|
||||
}
|
||||
|
||||
function buildAdminCard(order) {
|
||||
const card = document.createElement('article');
|
||||
card.className = 'admin-order-card';
|
||||
const card = document.createElement("article");
|
||||
card.className = "admin-order-card";
|
||||
card.dataset.email = order.employee_email;
|
||||
const isEditing = editingOrder === order.employee_email;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'admin-card-header';
|
||||
const employee = document.createElement('div');
|
||||
employee.className = 'admin-card-employee';
|
||||
const name = document.createElement('strong');
|
||||
const header = document.createElement("div");
|
||||
header.className = "admin-card-header";
|
||||
const employee = document.createElement("div");
|
||||
employee.className = "admin-card-employee";
|
||||
const name = document.createElement("strong");
|
||||
name.textContent = order.employee_name;
|
||||
const email = document.createElement('span');
|
||||
email.className = 'admin-muted';
|
||||
const email = document.createElement("span");
|
||||
email.className = "admin-muted";
|
||||
email.textContent = order.employee_email;
|
||||
employee.appendChild(name);
|
||||
employee.appendChild(email);
|
||||
const time = document.createElement('span');
|
||||
time.className = 'admin-card-time';
|
||||
const time = document.createElement("span");
|
||||
time.className = "admin-card-time";
|
||||
time.textContent = formatSubmittedAt(order.submitted_at);
|
||||
header.appendChild(employee);
|
||||
header.appendChild(time);
|
||||
|
||||
const items = document.createElement('div');
|
||||
items.className = 'admin-card-items';
|
||||
const items = document.createElement("div");
|
||||
items.className = "admin-card-items";
|
||||
const total = populateOrderItems(items, order, isEditing);
|
||||
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'admin-card-footer';
|
||||
const totalWrap = document.createElement('div');
|
||||
totalWrap.className = 'admin-card-total';
|
||||
const totalLabel = document.createElement('span');
|
||||
totalLabel.textContent = 'Total';
|
||||
const totalValue = document.createElement('strong');
|
||||
const footer = document.createElement("div");
|
||||
footer.className = "admin-card-footer";
|
||||
const totalWrap = document.createElement("div");
|
||||
totalWrap.className = "admin-card-total";
|
||||
const totalLabel = document.createElement("span");
|
||||
totalLabel.textContent = "Total";
|
||||
const totalValue = document.createElement("strong");
|
||||
totalValue.textContent = total;
|
||||
totalWrap.appendChild(totalLabel);
|
||||
totalWrap.appendChild(totalValue);
|
||||
|
|
@ -254,7 +271,7 @@ function buildAdminCard(order) {
|
|||
footer.appendChild(
|
||||
isEditing
|
||||
? buildEditActions(order.employee_email)
|
||||
: buildViewActions(order.employee_email, order.employee_name)
|
||||
: buildViewActions(order.employee_email, order.employee_name),
|
||||
);
|
||||
|
||||
card.appendChild(header);
|
||||
|
|
@ -264,9 +281,9 @@ function buildAdminCard(order) {
|
|||
}
|
||||
|
||||
function renderAdminCards(data) {
|
||||
const cards = document.getElementById('admin-cards');
|
||||
cards.innerHTML = '';
|
||||
(data.orders || []).forEach(order => {
|
||||
const cards = document.getElementById("admin-cards");
|
||||
cards.innerHTML = "";
|
||||
(data.orders || []).forEach((order) => {
|
||||
cards.appendChild(buildAdminCard(order));
|
||||
});
|
||||
}
|
||||
|
|
@ -274,75 +291,74 @@ function renderAdminCards(data) {
|
|||
function renderAdminTable(data) {
|
||||
lastAdminData = data;
|
||||
const hasOrders = !!(data.orders && data.orders.length > 0);
|
||||
const dlBtn = document.getElementById('admin-download-btn');
|
||||
const dlBtn = document.getElementById("admin-download-btn");
|
||||
if (dlBtn) dlBtn.disabled = !hasOrders;
|
||||
const tbody = document.getElementById('admin-tbody');
|
||||
tbody.innerHTML = '';
|
||||
const cards = document.getElementById('admin-cards');
|
||||
cards.innerHTML = '';
|
||||
const tbody = document.getElementById("admin-tbody");
|
||||
tbody.innerHTML = "";
|
||||
const cards = document.getElementById("admin-cards");
|
||||
cards.innerHTML = "";
|
||||
|
||||
if (!hasOrders) {
|
||||
document.getElementById('admin-empty').classList.remove('is-hidden');
|
||||
document.getElementById('admin-stats').textContent = '';
|
||||
document.getElementById("admin-empty").classList.remove("is-hidden");
|
||||
document.getElementById("admin-stats").textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('admin-empty').classList.add('is-hidden');
|
||||
const statsEl = document.getElementById('admin-stats');
|
||||
document.getElementById("admin-empty").classList.add("is-hidden");
|
||||
const statsEl = document.getElementById("admin-stats");
|
||||
statsEl.replaceChildren();
|
||||
const empStrong = document.createElement('strong');
|
||||
const empStrong = document.createElement("strong");
|
||||
empStrong.textContent = String(data.total_employees);
|
||||
statsEl.appendChild(empStrong);
|
||||
statsEl.appendChild(
|
||||
document.createTextNode(
|
||||
' employee' + (data.total_employees !== 1 ? 's' : '') + ' ordered · '
|
||||
)
|
||||
" employee" + (data.total_employees !== 1 ? "s" : "") + " ordered · ",
|
||||
),
|
||||
);
|
||||
const totalStrong = document.createElement('strong');
|
||||
totalStrong.textContent = '$' + data.grand_total.toFixed(2);
|
||||
const totalStrong = document.createElement("strong");
|
||||
totalStrong.textContent = "$" + data.grand_total.toFixed(2);
|
||||
statsEl.appendChild(totalStrong);
|
||||
statsEl.appendChild(document.createTextNode(' total payroll deductions'));
|
||||
statsEl.appendChild(document.createTextNode(" total payroll deductions"));
|
||||
|
||||
data.orders.forEach((order, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.id = 'admin-row-' + idx;
|
||||
const tr = document.createElement("tr");
|
||||
tr.id = "admin-row-" + idx;
|
||||
const isEditing = editingOrder === order.employee_email;
|
||||
|
||||
const empTd = document.createElement('td');
|
||||
const nameStrong = document.createElement('strong');
|
||||
const empTd = document.createElement("td");
|
||||
const nameStrong = document.createElement("strong");
|
||||
nameStrong.textContent = order.employee_name;
|
||||
empTd.appendChild(nameStrong);
|
||||
empTd.appendChild(document.createElement('br'));
|
||||
const emailSpan = document.createElement('span');
|
||||
emailSpan.className = 'admin-muted';
|
||||
empTd.appendChild(document.createElement("br"));
|
||||
const emailSpan = document.createElement("span");
|
||||
emailSpan.className = "admin-muted";
|
||||
emailSpan.textContent = order.employee_email;
|
||||
empTd.appendChild(emailSpan);
|
||||
|
||||
const itemsTd = document.createElement('td');
|
||||
itemsTd.className = 'items-cell';
|
||||
const itemsTd = document.createElement("td");
|
||||
itemsTd.className = "items-cell";
|
||||
|
||||
const totalHtml = populateOrderItems(
|
||||
itemsTd,
|
||||
order,
|
||||
isEditing,
|
||||
() => document.createElement('br')
|
||||
const totalHtml = populateOrderItems(itemsTd, order, isEditing, () =>
|
||||
document.createElement("br"),
|
||||
);
|
||||
tr.appendChild(empTd);
|
||||
tr.appendChild(itemsTd);
|
||||
|
||||
const totalTd = document.createElement('td');
|
||||
totalTd.className = 'total-cell';
|
||||
const totalTd = document.createElement("td");
|
||||
totalTd.className = "total-cell";
|
||||
totalTd.textContent = totalHtml;
|
||||
|
||||
const timeTd = document.createElement('td');
|
||||
timeTd.className = 'time-cell';
|
||||
const timeTd = document.createElement("td");
|
||||
timeTd.className = "time-cell";
|
||||
timeTd.textContent = formatSubmittedAt(order.submitted_at);
|
||||
|
||||
const actionsTd = document.createElement('td');
|
||||
const actionsTd = document.createElement("td");
|
||||
if (isEditing) {
|
||||
actionsTd.appendChild(buildEditActions(order.employee_email));
|
||||
} else {
|
||||
actionsTd.appendChild(buildViewActions(order.employee_email, order.employee_name));
|
||||
actionsTd.appendChild(
|
||||
buildViewActions(order.employee_email, order.employee_name),
|
||||
);
|
||||
}
|
||||
|
||||
tr.appendChild(totalTd);
|
||||
|
|
@ -351,21 +367,24 @@ function renderAdminTable(data) {
|
|||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
const totalRow = document.createElement('tr');
|
||||
totalRow.className = 'admin-total-row';
|
||||
const mealCount = data.orders.reduce((s, o) => s + o.items.reduce((a, i) => a + i.quantity, 0), 0);
|
||||
const t1 = document.createElement('td');
|
||||
t1.textContent = 'Total';
|
||||
const t2 = document.createElement('td');
|
||||
t2.textContent = mealCount + (mealCount === 1 ? ' meal' : ' meals');
|
||||
const t3 = document.createElement('td');
|
||||
t3.className = 'total-cell';
|
||||
t3.textContent = '$' + data.grand_total.toFixed(2);
|
||||
const totalRow = document.createElement("tr");
|
||||
totalRow.className = "admin-total-row";
|
||||
const mealCount = data.orders.reduce(
|
||||
(s, o) => s + o.items.reduce((a, i) => a + i.quantity, 0),
|
||||
0,
|
||||
);
|
||||
const t1 = document.createElement("td");
|
||||
t1.textContent = "Total";
|
||||
const t2 = document.createElement("td");
|
||||
t2.textContent = mealCount + (mealCount === 1 ? " meal" : " meals");
|
||||
const t3 = document.createElement("td");
|
||||
t3.className = "total-cell";
|
||||
t3.textContent = "$" + data.grand_total.toFixed(2);
|
||||
totalRow.appendChild(t1);
|
||||
totalRow.appendChild(t2);
|
||||
totalRow.appendChild(t3);
|
||||
totalRow.appendChild(document.createElement('td'));
|
||||
totalRow.appendChild(document.createElement('td'));
|
||||
totalRow.appendChild(document.createElement("td"));
|
||||
totalRow.appendChild(document.createElement("td"));
|
||||
tbody.appendChild(totalRow);
|
||||
renderAdminCards(data);
|
||||
}
|
||||
|
|
@ -374,24 +393,28 @@ function downloadOrderList() {
|
|||
const data = lastAdminData;
|
||||
if (!data || !data.orders || data.orders.length === 0) return;
|
||||
const totals = {};
|
||||
data.orders.forEach(o => (o.items || []).forEach(i => {
|
||||
const name = i.name || '';
|
||||
data.orders.forEach((o) =>
|
||||
(o.items || []).forEach((i) => {
|
||||
const name = i.name || "";
|
||||
totals[name] = (totals[name] || 0) + (i.quantity || 0);
|
||||
}));
|
||||
}),
|
||||
);
|
||||
const names = Object.keys(totals).sort((a, b) => a.localeCompare(b));
|
||||
const esc = v => {
|
||||
const esc = (v) => {
|
||||
const s = String(v);
|
||||
const safe = /^[=+\-@\t\r]/.test(s) ? "'" + s : s;
|
||||
return /[",\n]/.test(safe) ? '"' + safe.replace(/"/g, '""') + '"' : safe;
|
||||
};
|
||||
const rows = [['Item', 'Quantity']].concat(names.map(n => [n, totals[n]]));
|
||||
const csv = rows.map(r => r.map(esc).join(',')).join('\r\n');
|
||||
const rows = [["Item", "Quantity"]].concat(
|
||||
names.map((n) => [n, totals[n]]),
|
||||
);
|
||||
const csv = rows.map((r) => r.map(esc).join(",")).join("\r\n");
|
||||
const week = data.week || currentAdminWeek || WEEK;
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = 'order-list-' + week + '.csv';
|
||||
a.download = "order-list-" + week + ".csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
|
@ -400,69 +423,85 @@ function downloadOrderList() {
|
|||
|
||||
function downloadSummaryPdf() {
|
||||
const week = currentAdminWeek || WEEK;
|
||||
const btn = document.getElementById('admin-pdf-btn');
|
||||
const btn = document.getElementById("admin-pdf-btn");
|
||||
if (btn) btn.disabled = true;
|
||||
window.FormUtils.clearStatus('admin');
|
||||
adminFetch(ADMIN_PDF_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() })
|
||||
.then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
|
||||
window.FormUtils.clearStatus("admin");
|
||||
adminFetch(ADMIN_PDF_URL + "?week=" + encodeURIComponent(week), {
|
||||
headers: adminHeaders(),
|
||||
})
|
||||
.then((r) =>
|
||||
r.json().then((data) => ({ ok: r.ok, status: r.status, data })),
|
||||
)
|
||||
.then(({ ok, status, data }) => {
|
||||
if (ok && data.url) {
|
||||
// Only follow https presigned URLs from our admin API.
|
||||
if (typeof data.url === 'string' && data.url.startsWith('https://')) {
|
||||
if (typeof data.url === "string" && data.url.startsWith("https://")) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
window.FormUtils.showStatus('Could not fetch the summary PDF: invalid URL', {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
});
|
||||
window.FormUtils.showStatus(
|
||||
"Could not fetch the summary PDF: invalid URL",
|
||||
{
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (status === 404) {
|
||||
window.FormUtils.showStatus(
|
||||
'No summary PDF for ' + week + ' yet — it is generated when the week closes on Thursday.',
|
||||
{ tone: 'info', scope: 'admin' }
|
||||
"No summary PDF for " +
|
||||
week +
|
||||
" yet — it is generated when the week closes on Thursday.",
|
||||
{ tone: "info", scope: "admin" },
|
||||
);
|
||||
} else {
|
||||
window.FormUtils.showStatus(
|
||||
'Could not fetch the summary PDF: ' + (data.error || 'unknown error'),
|
||||
{ tone: 'error', scope: 'admin' }
|
||||
"Could not fetch the summary PDF: " +
|
||||
(data.error || "unknown error"),
|
||||
{ tone: "error", scope: "admin" },
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
window.FormUtils.showStatus('Could not fetch the summary PDF.', {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
window.FormUtils.showStatus("Could not fetch the summary PDF.", {
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
});
|
||||
})
|
||||
.finally(() => { if (btn) btn.disabled = false; });
|
||||
.finally(() => {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function adminStartEdit(email) {
|
||||
window.FormUtils.clearStatus('admin');
|
||||
window.FormUtils.clearStatus("admin");
|
||||
const requestedWeek = currentAdminWeek;
|
||||
setAdminActionDisabled('start-edit', true);
|
||||
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(requestedWeek), { headers: adminHeaders() })
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
setAdminActionDisabled("start-edit", true);
|
||||
adminFetch(ADMIN_URL + "?week=" + encodeURIComponent(requestedWeek), {
|
||||
headers: adminHeaders(),
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
.then((data) => {
|
||||
if (currentAdminWeek !== requestedWeek) return;
|
||||
lastAdminData = data;
|
||||
const order = data.orders.find(o => o.employee_email === email);
|
||||
const order = data.orders.find((o) => o.employee_email === email);
|
||||
if (!order) return;
|
||||
editingOrder = email;
|
||||
editQuantities = {};
|
||||
order.items.forEach(i => { editQuantities[i.name] = i.quantity; });
|
||||
order.items.forEach((i) => {
|
||||
editQuantities[i.name] = i.quantity;
|
||||
});
|
||||
renderAdminTable(data);
|
||||
})
|
||||
.catch(() => {
|
||||
window.FormUtils.showStatus('Failed to load this order for editing.', {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
window.FormUtils.showStatus("Failed to load this order for editing.", {
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
});
|
||||
})
|
||||
.finally(() => setAdminActionDisabled('start-edit', false));
|
||||
.finally(() => setAdminActionDisabled("start-edit", false));
|
||||
}
|
||||
|
||||
function adminCancelEdit() {
|
||||
|
|
@ -489,69 +528,92 @@ function adminSaveEdit(email) {
|
|||
.map(([name, quantity]) => ({ name, quantity }));
|
||||
if (items.length === 0) {
|
||||
// confirm() retained deliberately for destructive zero-qty → delete guard.
|
||||
if (confirm('All quantities are zero. Delete this order instead?')) {
|
||||
adminDeleteOrder(email, '');
|
||||
if (confirm("All quantities are zero. Delete this order instead?")) {
|
||||
adminDeleteOrder(email, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
window.FormUtils.clearStatus('admin');
|
||||
setAdminActionDisabled('save-edit', true);
|
||||
window.FormUtils.clearStatus("admin");
|
||||
setAdminActionDisabled("save-edit", true);
|
||||
adminFetch(ADMIN_URL, {
|
||||
method: 'PUT',
|
||||
headers: { ...adminHeaders(), 'Content-Type': 'application/json' },
|
||||
method: "PUT",
|
||||
headers: { ...adminHeaders(), "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ week: currentAdminWeek, email, items }),
|
||||
})
|
||||
.then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); })
|
||||
.then(() => { editingOrder = null; editQuantities = {}; loadWeekOrders(currentAdminWeek); })
|
||||
.catch(e => {
|
||||
window.FormUtils.showStatus('Failed to update: ' + e.message, {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
.then((r) => {
|
||||
if (!r.ok)
|
||||
return r.json().then((d) => {
|
||||
throw new Error(d.error);
|
||||
});
|
||||
return r.json();
|
||||
})
|
||||
.then(() => {
|
||||
editingOrder = null;
|
||||
editQuantities = {};
|
||||
loadWeekOrders(currentAdminWeek);
|
||||
})
|
||||
.catch((e) => {
|
||||
window.FormUtils.showStatus("Failed to update: " + e.message, {
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
});
|
||||
})
|
||||
.finally(() => setAdminActionDisabled('save-edit', false));
|
||||
.finally(() => setAdminActionDisabled("save-edit", false));
|
||||
}
|
||||
|
||||
function adminDeleteOrder(email, name) {
|
||||
const label = name ? name + ' (' + email + ')' : email;
|
||||
const label = name ? name + " (" + email + ")" : email;
|
||||
// confirm() retained deliberately for destructive delete guard.
|
||||
if (!confirm('Delete order for ' + label + '?')) return;
|
||||
window.FormUtils.clearStatus('admin');
|
||||
setAdminActionDisabled('delete', true);
|
||||
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek) + '&email=' + encodeURIComponent(email), {
|
||||
method: 'DELETE',
|
||||
if (!confirm("Delete order for " + label + "?")) return;
|
||||
window.FormUtils.clearStatus("admin");
|
||||
setAdminActionDisabled("delete", true);
|
||||
adminFetch(
|
||||
ADMIN_URL +
|
||||
"?week=" +
|
||||
encodeURIComponent(currentAdminWeek) +
|
||||
"&email=" +
|
||||
encodeURIComponent(email),
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: adminHeaders(),
|
||||
},
|
||||
)
|
||||
.then((r) => {
|
||||
if (!r.ok)
|
||||
return r.json().then((d) => {
|
||||
throw new Error(d.error);
|
||||
});
|
||||
return r.json();
|
||||
})
|
||||
.then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); })
|
||||
.then(() => loadWeekOrders(currentAdminWeek))
|
||||
.catch(e => {
|
||||
window.FormUtils.showStatus('Failed to delete: ' + e.message, {
|
||||
tone: 'error',
|
||||
scope: 'admin',
|
||||
.catch((e) => {
|
||||
window.FormUtils.showStatus("Failed to delete: " + e.message, {
|
||||
tone: "error",
|
||||
scope: "admin",
|
||||
});
|
||||
})
|
||||
.finally(() => setAdminActionDisabled('delete', false));
|
||||
.finally(() => setAdminActionDisabled("delete", false));
|
||||
}
|
||||
|
||||
function handleAdminPanelClick(e) {
|
||||
const btn = e.target.closest('[data-admin-action]');
|
||||
const btn = e.target.closest("[data-admin-action]");
|
||||
if (btn) {
|
||||
const action = btn.dataset.adminAction;
|
||||
if (action === 'start-edit') {
|
||||
if (action === "start-edit") {
|
||||
adminStartEdit(btn.dataset.email);
|
||||
} else if (action === 'delete') {
|
||||
adminDeleteOrder(btn.dataset.email, btn.dataset.name || '');
|
||||
} else if (action === 'save-edit') {
|
||||
} else if (action === "delete") {
|
||||
adminDeleteOrder(btn.dataset.email, btn.dataset.name || "");
|
||||
} else if (action === "save-edit") {
|
||||
adminSaveEdit(btn.dataset.email);
|
||||
} else if (action === 'cancel-edit') {
|
||||
} else if (action === "cancel-edit") {
|
||||
adminCancelEdit();
|
||||
} else if (action === 'edit-qty') {
|
||||
} else if (action === "edit-qty") {
|
||||
adminEditQty(btn.dataset.mealName, parseInt(btn.dataset.delta, 10));
|
||||
} else if (action === 'download-list') {
|
||||
} else if (action === "download-list") {
|
||||
downloadOrderList();
|
||||
} else if (action === 'download-pdf') {
|
||||
} else if (action === "download-pdf") {
|
||||
downloadSummaryPdf();
|
||||
} else if (action === 'hide-admin') {
|
||||
} else if (action === "hide-admin") {
|
||||
window.hideAdmin();
|
||||
}
|
||||
return;
|
||||
|
|
@ -559,11 +621,13 @@ function handleAdminPanelClick(e) {
|
|||
}
|
||||
|
||||
function adminInit() {
|
||||
const panel = document.getElementById('admin-panel');
|
||||
const panel = document.getElementById("admin-panel");
|
||||
if (panel) {
|
||||
panel.addEventListener('click', handleAdminPanelClick);
|
||||
panel.addEventListener('change', (e) => {
|
||||
const select = e.target.closest('[data-admin-action="add-item-select"]');
|
||||
panel.addEventListener("click", handleAdminPanelClick);
|
||||
panel.addEventListener("change", (e) => {
|
||||
const select = e.target.closest(
|
||||
'[data-admin-action="add-item-select"]',
|
||||
);
|
||||
if (select && select.value) {
|
||||
adminAddItem(select.value);
|
||||
select.selectedIndex = 0;
|
||||
|
|
@ -571,9 +635,11 @@ function adminInit() {
|
|||
});
|
||||
}
|
||||
|
||||
const weekSelect = document.getElementById('admin-week');
|
||||
const weekSelect = document.getElementById("admin-week");
|
||||
if (weekSelect) {
|
||||
weekSelect.addEventListener('change', () => loadWeekOrders(weekSelect.value));
|
||||
weekSelect.addEventListener("change", () =>
|
||||
loadWeekOrders(weekSelect.value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const ADMIN_PDF_URL = CONFIG.adminPdfUrl;
|
|||
const WEEK = CONFIG.week;
|
||||
const BULK_DISCOUNT = CONFIG.bulkDiscount;
|
||||
const COMPANY_SUBSIDY = CONFIG.companySubsidy;
|
||||
const GOOGLE_CLIENT_ID = CONFIG.googleClientId || '';
|
||||
const GOOGLE_CLIENT_ID = CONFIG.googleClientId || "";
|
||||
|
||||
window.MEALS = MEALS;
|
||||
window.WEEK = WEEK;
|
||||
|
|
@ -34,33 +34,35 @@ window.googleCredential = null;
|
|||
|
||||
function employeePrice(retail) {
|
||||
if (!retail) return 0;
|
||||
const bulkPrice = Math.round(retail * (1 - BULK_DISCOUNT / 100) * 100 + 1e-8) / 100;
|
||||
const empPrice = Math.round(bulkPrice * (1 - COMPANY_SUBSIDY / 100) * 100 + 1e-8) / 100;
|
||||
const bulkPrice =
|
||||
Math.round(retail * (1 - BULK_DISCOUNT / 100) * 100 + 1e-8) / 100;
|
||||
const empPrice =
|
||||
Math.round(bulkPrice * (1 - COMPANY_SUBSIDY / 100) * 100 + 1e-8) / 100;
|
||||
return empPrice;
|
||||
}
|
||||
|
||||
function clearStatus(scope) {
|
||||
const prefix = scope === 'admin' ? 'admin-status' : 'form-status';
|
||||
const alertEl = document.getElementById(prefix + '-alert');
|
||||
const infoEl = document.getElementById(prefix + '-info');
|
||||
if (alertEl) alertEl.textContent = '';
|
||||
if (infoEl) infoEl.textContent = '';
|
||||
const prefix = scope === "admin" ? "admin-status" : "form-status";
|
||||
const alertEl = document.getElementById(prefix + "-alert");
|
||||
const infoEl = document.getElementById(prefix + "-info");
|
||||
if (alertEl) alertEl.textContent = "";
|
||||
if (infoEl) infoEl.textContent = "";
|
||||
}
|
||||
|
||||
function showStatus(message, options) {
|
||||
const opts = options || {};
|
||||
const tone = opts.tone || 'error';
|
||||
const scope = opts.scope || 'form';
|
||||
const prefix = scope === 'admin' ? 'admin-status' : 'form-status';
|
||||
const alertEl = document.getElementById(prefix + '-alert');
|
||||
const infoEl = document.getElementById(prefix + '-info');
|
||||
const tone = opts.tone || "error";
|
||||
const scope = opts.scope || "form";
|
||||
const prefix = scope === "admin" ? "admin-status" : "form-status";
|
||||
const alertEl = document.getElementById(prefix + "-alert");
|
||||
const infoEl = document.getElementById(prefix + "-info");
|
||||
if (!alertEl || !infoEl) return;
|
||||
if (tone === 'info') {
|
||||
alertEl.textContent = '';
|
||||
infoEl.textContent = message || '';
|
||||
if (tone === "info") {
|
||||
alertEl.textContent = "";
|
||||
infoEl.textContent = message || "";
|
||||
} else {
|
||||
infoEl.textContent = '';
|
||||
alertEl.textContent = message || '';
|
||||
infoEl.textContent = "";
|
||||
alertEl.textContent = message || "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,16 +81,17 @@ function buildItems() {
|
|||
}
|
||||
|
||||
function waitForGoogleAuth(attempt = 0) {
|
||||
if (typeof google !== 'undefined' && google.accounts && google.accounts.id) {
|
||||
if (typeof google !== "undefined" && google.accounts && google.accounts.id) {
|
||||
initGoogleAuth();
|
||||
} else if (attempt < GOOGLE_AUTH_MAX_ATTEMPTS) {
|
||||
setTimeout(() => waitForGoogleAuth(attempt + 1), 50);
|
||||
} else {
|
||||
const signIn = document.getElementById('g-signin-btn');
|
||||
const signIn = document.getElementById("g-signin-btn");
|
||||
if (signIn) {
|
||||
signIn.textContent = 'Google sign-in failed to load. Refresh the page to try again.';
|
||||
signIn.textContent =
|
||||
"Google sign-in failed to load. Refresh the page to try again.";
|
||||
}
|
||||
console.error('Google Identity Services failed to load.');
|
||||
console.error("Google Identity Services failed to load.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,30 +101,35 @@ function initGoogleAuth() {
|
|||
callback: handleCredentialResponse,
|
||||
auto_select: true,
|
||||
});
|
||||
google.accounts.id.renderButton(
|
||||
document.getElementById('g-signin-btn'),
|
||||
{ theme: 'outline', size: 'large', text: 'signin_with', width: 300 }
|
||||
);
|
||||
google.accounts.id.renderButton(document.getElementById("g-signin-btn"), {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
text: "signin_with",
|
||||
width: 300,
|
||||
});
|
||||
}
|
||||
|
||||
function handleCredentialResponse(response) {
|
||||
googleCredential = response.credential;
|
||||
syncGoogleCredential();
|
||||
const b64 = response.credential.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
||||
const payloadBytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
||||
const b64 = response.credential
|
||||
.split(".")[1]
|
||||
.replace(/-/g, "+")
|
||||
.replace(/_/g, "/");
|
||||
const payloadBytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||
const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
|
||||
googleUser = { name: payload.name, email: payload.email };
|
||||
|
||||
document.getElementById('auth-overlay').classList.add('is-hidden');
|
||||
document.getElementById('app').classList.remove('is-hidden');
|
||||
const footer = document.getElementById('sticky-footer');
|
||||
if (footer) footer.classList.remove('is-hidden');
|
||||
document.getElementById('user-name').textContent = payload.name;
|
||||
document.getElementById('user-email').textContent = payload.email;
|
||||
document.getElementById("auth-overlay").classList.add("is-hidden");
|
||||
document.getElementById("app").classList.remove("is-hidden");
|
||||
const footer = document.getElementById("sticky-footer");
|
||||
if (footer) footer.classList.remove("is-hidden");
|
||||
document.getElementById("user-name").textContent = payload.name;
|
||||
document.getElementById("user-email").textContent = payload.email;
|
||||
if (payload.picture) {
|
||||
const avatar = document.getElementById('user-avatar');
|
||||
const avatar = document.getElementById("user-avatar");
|
||||
avatar.src = payload.picture;
|
||||
avatar.classList.remove('is-hidden');
|
||||
avatar.classList.remove("is-hidden");
|
||||
}
|
||||
|
||||
updateTotal();
|
||||
|
|
@ -133,37 +141,49 @@ function handleCredentialResponse(response) {
|
|||
|
||||
function signOut() {
|
||||
isAdmin = false;
|
||||
const ab = document.getElementById('admin-btn');
|
||||
if (ab) ab.classList.add('is-hidden');
|
||||
const banner = document.getElementById('admin-closed-banner');
|
||||
const ab = document.getElementById("admin-btn");
|
||||
if (ab) ab.classList.add("is-hidden");
|
||||
const banner = document.getElementById("admin-closed-banner");
|
||||
if (banner) banner.remove();
|
||||
googleCredential = null;
|
||||
googleUser = null;
|
||||
syncGoogleCredential();
|
||||
if (CONFIG.authMode === 'google' && typeof google !== 'undefined' && google.accounts) {
|
||||
if (
|
||||
CONFIG.authMode === "google" &&
|
||||
typeof google !== "undefined" &&
|
||||
google.accounts
|
||||
) {
|
||||
google.accounts.id.disableAutoSelect();
|
||||
}
|
||||
document.getElementById('auth-overlay').classList.remove('is-hidden');
|
||||
document.getElementById('app').classList.add('is-hidden');
|
||||
const footer = document.getElementById('sticky-footer');
|
||||
if (footer) footer.classList.add('is-hidden');
|
||||
document.getElementById("auth-overlay").classList.remove("is-hidden");
|
||||
document.getElementById("app").classList.add("is-hidden");
|
||||
const footer = document.getElementById("sticky-footer");
|
||||
if (footer) footer.classList.add("is-hidden");
|
||||
if (formClosed) showClosedOverlay();
|
||||
}
|
||||
|
||||
function checkDuplicateOrder() {
|
||||
try {
|
||||
if (localStorage.getItem('lastOrderWeek') === WEEK) {
|
||||
document.getElementById('duplicate-warning').classList.remove('is-hidden');
|
||||
if (localStorage.getItem("lastOrderWeek") === WEEK) {
|
||||
document
|
||||
.getElementById("duplicate-warning")
|
||||
.classList.remove("is-hidden");
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function getActiveFilter() {
|
||||
return document.querySelector('.filters button.active')?.textContent || 'All Meals';
|
||||
return (
|
||||
document.querySelector(".filters button.active")?.textContent || "All Meals"
|
||||
);
|
||||
}
|
||||
|
||||
function mealMatchesFilter(meal, activeFilter, search) {
|
||||
if (activeFilter !== 'All Meals' && !(meal.dietary_tags || []).includes(activeFilter)) return false;
|
||||
if (
|
||||
activeFilter !== "All Meals" &&
|
||||
!(meal.dietary_tags || []).includes(activeFilter)
|
||||
)
|
||||
return false;
|
||||
if (search && !meal.name.toLowerCase().includes(search)) return false;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -171,37 +191,45 @@ function mealMatchesFilter(meal, activeFilter, search) {
|
|||
function buildMealMetaHtml(meal) {
|
||||
const hasDiscount = CONFIG.hasDiscount && meal.price;
|
||||
const empPrice = employeePrice(meal.price);
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'meal-meta';
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meal-meta";
|
||||
if (hasDiscount) {
|
||||
const retail = document.createElement('span');
|
||||
retail.className = 'price-retail';
|
||||
retail.textContent = '$' + meal.price.toFixed(2);
|
||||
const retail = document.createElement("span");
|
||||
retail.className = "price-retail";
|
||||
retail.textContent = "$" + meal.price.toFixed(2);
|
||||
meta.appendChild(retail);
|
||||
const employee = document.createElement('span');
|
||||
employee.className = 'price-employee';
|
||||
employee.textContent = '$' + empPrice.toFixed(2);
|
||||
const employee = document.createElement("span");
|
||||
employee.className = "price-employee";
|
||||
employee.textContent = "$" + empPrice.toFixed(2);
|
||||
meta.appendChild(employee);
|
||||
} else if (meal.price) {
|
||||
meta.appendChild(document.createTextNode('$' + meal.price.toFixed(2)));
|
||||
meta.appendChild(document.createTextNode("$" + meal.price.toFixed(2)));
|
||||
} else {
|
||||
meta.appendChild(document.createTextNode('—'));
|
||||
meta.appendChild(document.createTextNode("—"));
|
||||
}
|
||||
meta.appendChild(document.createTextNode(' · ' + (meal.calories || '?') + ' cal · ' + (meal.protein || '?') + ' protein'));
|
||||
meta.appendChild(
|
||||
document.createTextNode(
|
||||
" · " +
|
||||
(meal.calories || "?") +
|
||||
" cal · " +
|
||||
(meal.protein || "?") +
|
||||
" protein",
|
||||
),
|
||||
);
|
||||
return meta;
|
||||
}
|
||||
|
||||
function buildMealTagsEl(meal) {
|
||||
const tagsEl = document.createElement('div');
|
||||
tagsEl.className = 'meal-tags';
|
||||
const tagsEl = document.createElement("div");
|
||||
tagsEl.className = "meal-tags";
|
||||
if (meal.is_new) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'new';
|
||||
span.textContent = 'NEW';
|
||||
const span = document.createElement("span");
|
||||
span.className = "new";
|
||||
span.textContent = "NEW";
|
||||
tagsEl.appendChild(span);
|
||||
}
|
||||
(meal.dietary_tags || []).forEach(t => {
|
||||
const span = document.createElement('span');
|
||||
(meal.dietary_tags || []).forEach((t) => {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = t;
|
||||
tagsEl.appendChild(span);
|
||||
});
|
||||
|
|
@ -209,25 +237,25 @@ function buildMealTagsEl(meal) {
|
|||
}
|
||||
|
||||
function removeDescToggle(card) {
|
||||
const existing = card.querySelector('.meal-desc-toggle');
|
||||
const existing = card.querySelector(".meal-desc-toggle");
|
||||
if (existing) existing.remove();
|
||||
}
|
||||
|
||||
function attachDescToggleIfOverflow(card) {
|
||||
const descEl = card.querySelector('.meal-desc');
|
||||
if (!descEl || descEl.classList.contains('is-hidden')) return;
|
||||
const descEl = card.querySelector(".meal-desc");
|
||||
if (!descEl || descEl.classList.contains("is-hidden")) return;
|
||||
removeDescToggle(card);
|
||||
if (descEl.scrollHeight <= descEl.clientHeight + 1) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'meal-desc-toggle';
|
||||
btn.textContent = 'More';
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
btn.addEventListener('click', () => {
|
||||
const expanded = descEl.classList.toggle('is-expanded');
|
||||
btn.textContent = expanded ? 'Less' : 'More';
|
||||
btn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "meal-desc-toggle";
|
||||
btn.textContent = "More";
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
btn.addEventListener("click", () => {
|
||||
const expanded = descEl.classList.toggle("is-expanded");
|
||||
btn.textContent = expanded ? "Less" : "More";
|
||||
btn.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
});
|
||||
descEl.after(btn);
|
||||
}
|
||||
|
|
@ -241,57 +269,59 @@ function scheduleDescToggle(card) {
|
|||
function updateMealCard(card, meal, i) {
|
||||
const qty = quantities[i] || 0;
|
||||
card.dataset.mealIndex = String(i);
|
||||
card.classList.toggle('selected', qty > 0);
|
||||
card.classList.toggle("selected", qty > 0);
|
||||
|
||||
const img = card.querySelector('.meal-img');
|
||||
const img = card.querySelector(".meal-img");
|
||||
if (meal.image_url && /^https?:\/\//i.test(meal.image_url)) {
|
||||
img.src = meal.image_url;
|
||||
img.alt = meal.name;
|
||||
img.classList.remove('is-hidden');
|
||||
img.classList.remove("is-hidden");
|
||||
} else {
|
||||
img.removeAttribute('src');
|
||||
img.alt = '';
|
||||
img.classList.add('is-hidden');
|
||||
img.removeAttribute("src");
|
||||
img.alt = "";
|
||||
img.classList.add("is-hidden");
|
||||
}
|
||||
|
||||
card.querySelector('.meal-name').textContent = meal.name;
|
||||
card.querySelector(".meal-name").textContent = meal.name;
|
||||
|
||||
const descEl = card.querySelector('.meal-desc');
|
||||
const descEl = card.querySelector(".meal-desc");
|
||||
removeDescToggle(card);
|
||||
descEl.classList.remove('is-expanded');
|
||||
descEl.classList.remove("is-expanded");
|
||||
if (meal.description) {
|
||||
descEl.textContent = meal.description;
|
||||
descEl.classList.remove('is-hidden');
|
||||
descEl.classList.remove("is-hidden");
|
||||
scheduleDescToggle(card);
|
||||
} else {
|
||||
descEl.textContent = '';
|
||||
descEl.classList.add('is-hidden');
|
||||
descEl.textContent = "";
|
||||
descEl.classList.add("is-hidden");
|
||||
}
|
||||
|
||||
const metaEl = card.querySelector('.meal-meta');
|
||||
const metaEl = card.querySelector(".meal-meta");
|
||||
const newMeta = buildMealMetaHtml(meal);
|
||||
metaEl.replaceWith(newMeta);
|
||||
|
||||
const tagsEl = card.querySelector('.meal-tags');
|
||||
const tagsEl = card.querySelector(".meal-tags");
|
||||
const newTags = buildMealTagsEl(meal);
|
||||
tagsEl.replaceWith(newTags);
|
||||
|
||||
const qtyInput = card.querySelector('.qty');
|
||||
const qtyInput = card.querySelector(".qty");
|
||||
qtyInput.value = qty;
|
||||
qtyInput.setAttribute('aria-label', 'Quantity of ' + meal.name);
|
||||
qtyInput.setAttribute("aria-label", "Quantity of " + meal.name);
|
||||
|
||||
const minusBtn = card.querySelector('[data-qty-delta="-1"]');
|
||||
const plusBtn = card.querySelector('[data-qty-delta="1"]');
|
||||
if (minusBtn) minusBtn.setAttribute('aria-label', 'Remove one ' + meal.name);
|
||||
if (plusBtn) plusBtn.setAttribute('aria-label', 'Add one ' + meal.name);
|
||||
if (minusBtn) minusBtn.setAttribute("aria-label", "Remove one " + meal.name);
|
||||
if (plusBtn) plusBtn.setAttribute("aria-label", "Add one " + meal.name);
|
||||
|
||||
const disabled = formClosed && !isAdmin;
|
||||
card.querySelectorAll('[data-qty-delta]').forEach(btn => { btn.disabled = disabled; });
|
||||
card.querySelectorAll("[data-qty-delta]").forEach((btn) => {
|
||||
btn.disabled = disabled;
|
||||
});
|
||||
qtyInput.disabled = disabled;
|
||||
}
|
||||
|
||||
function createMealCard(meal, i) {
|
||||
const tpl = document.getElementById('meal-card-tpl');
|
||||
const tpl = document.getElementById("meal-card-tpl");
|
||||
const card = tpl.content.firstElementChild.cloneNode(true);
|
||||
updateMealCard(card, meal, i);
|
||||
return card;
|
||||
|
|
@ -299,9 +329,9 @@ function createMealCard(meal, i) {
|
|||
|
||||
function renderMeals() {
|
||||
const activeFilter = getActiveFilter();
|
||||
const search = document.getElementById('search').value.toLowerCase();
|
||||
const list = document.getElementById('meals-list');
|
||||
list.innerHTML = '';
|
||||
const search = document.getElementById("search").value.toLowerCase();
|
||||
const list = document.getElementById("meals-list");
|
||||
list.innerHTML = "";
|
||||
|
||||
MEALS.forEach((meal, i) => {
|
||||
if (!mealMatchesFilter(meal, activeFilter, search)) return;
|
||||
|
|
@ -319,8 +349,8 @@ function updateMealCardQty(index) {
|
|||
const card = findMealCard(index);
|
||||
if (!card) return;
|
||||
const qty = quantities[index] || 0;
|
||||
card.classList.toggle('selected', qty > 0);
|
||||
const qtyInput = card.querySelector('.qty');
|
||||
card.classList.toggle("selected", qty > 0);
|
||||
const qtyInput = card.querySelector(".qty");
|
||||
if (qtyInput && qtyInput !== document.activeElement) qtyInput.value = qty;
|
||||
}
|
||||
|
||||
|
|
@ -344,29 +374,29 @@ function changeQty(index, delta) {
|
|||
function bindMealsListDelegation() {
|
||||
if (mealsListDelegationBound) return;
|
||||
mealsListDelegationBound = true;
|
||||
const list = document.getElementById('meals-list');
|
||||
list.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-qty-delta]');
|
||||
const list = document.getElementById("meals-list");
|
||||
list.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("[data-qty-delta]");
|
||||
if (!btn) return;
|
||||
const card = btn.closest('[data-meal-index]');
|
||||
const card = btn.closest("[data-meal-index]");
|
||||
if (!card) return;
|
||||
const index = parseInt(card.dataset.mealIndex, 10);
|
||||
const delta = parseInt(btn.dataset.qtyDelta, 10);
|
||||
changeQty(index, delta);
|
||||
});
|
||||
list.addEventListener('input', (e) => {
|
||||
const input = e.target.closest('.qty');
|
||||
list.addEventListener("input", (e) => {
|
||||
const input = e.target.closest(".qty");
|
||||
if (!input) return;
|
||||
const card = input.closest('[data-meal-index]');
|
||||
const card = input.closest("[data-meal-index]");
|
||||
if (!card) return;
|
||||
const index = parseInt(card.dataset.mealIndex, 10);
|
||||
setQtyState(index, input.value);
|
||||
updateTotal();
|
||||
});
|
||||
list.addEventListener('focusout', (e) => {
|
||||
const input = e.target.closest('.qty');
|
||||
list.addEventListener("focusout", (e) => {
|
||||
const input = e.target.closest(".qty");
|
||||
if (!input) return;
|
||||
const card = input.closest('[data-meal-index]');
|
||||
const card = input.closest("[data-meal-index]");
|
||||
if (!card) return;
|
||||
updateMealCardQty(parseInt(card.dataset.mealIndex, 10));
|
||||
});
|
||||
|
|
@ -379,98 +409,118 @@ function updateTotal() {
|
|||
total += employeePrice(MEALS[i].price || 0) * qty;
|
||||
count += qty;
|
||||
});
|
||||
document.getElementById('total-display').textContent = '$' + total.toFixed(2);
|
||||
document.getElementById('count-display').textContent = count + ' meal' + (count !== 1 ? 's' : '');
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
submitBtn.disabled = count === 0 || (CONFIG.authMode === 'google' && !googleCredential);
|
||||
document.getElementById("total-display").textContent = "$" + total.toFixed(2);
|
||||
document.getElementById("count-display").textContent =
|
||||
count + " meal" + (count !== 1 ? "s" : "");
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
submitBtn.disabled =
|
||||
count === 0 || (CONFIG.authMode === "google" && !googleCredential);
|
||||
}
|
||||
|
||||
async function submitOrder() {
|
||||
clearStatus('form');
|
||||
clearStatus("form");
|
||||
|
||||
if (formClosed && !isAdmin) {
|
||||
showStatus('Orders are closed.', { tone: 'error', scope: 'form' });
|
||||
showStatus("Orders are closed.", { tone: "error", scope: "form" });
|
||||
return;
|
||||
}
|
||||
|
||||
let payload;
|
||||
let successName;
|
||||
|
||||
if (CONFIG.authMode === 'google') {
|
||||
if (CONFIG.authMode === "google") {
|
||||
if (!googleCredential || !googleUser) {
|
||||
showStatus('Please sign in with Google first.', { tone: 'error', scope: 'form' });
|
||||
showStatus("Please sign in with Google first.", {
|
||||
tone: "error",
|
||||
scope: "form",
|
||||
});
|
||||
return;
|
||||
}
|
||||
payload = { google_id_token: googleCredential, items: buildItems() };
|
||||
successName = googleUser.name;
|
||||
} else {
|
||||
const nameEl = document.getElementById('emp-name');
|
||||
const emailEl = document.getElementById('emp-email');
|
||||
const nameEl = document.getElementById("emp-name");
|
||||
const emailEl = document.getElementById("emp-email");
|
||||
const name = nameEl.value.trim();
|
||||
const email = emailEl.value.trim();
|
||||
if (!name) {
|
||||
showStatus('Please enter your name.', { tone: 'error', scope: 'form' });
|
||||
showStatus("Please enter your name.", { tone: "error", scope: "form" });
|
||||
nameEl.focus();
|
||||
return;
|
||||
}
|
||||
if (!email) {
|
||||
showStatus('Please enter your email.', { tone: 'error', scope: 'form' });
|
||||
showStatus("Please enter your email.", { tone: "error", scope: "form" });
|
||||
emailEl.focus();
|
||||
return;
|
||||
}
|
||||
payload = { employee_name: name, employee_email: email, items: buildItems() };
|
||||
payload = {
|
||||
employee_name: name,
|
||||
employee_email: email,
|
||||
items: buildItems(),
|
||||
};
|
||||
successName = name;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
const btn = document.getElementById("submit-btn");
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting...';
|
||||
btn.textContent = "Submitting...";
|
||||
|
||||
try {
|
||||
const res = await fetch(SUBMIT_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
for (const el of document.getElementById('app').children) {
|
||||
if (el.id !== 'success') el.classList.add('is-hidden');
|
||||
for (const el of document.getElementById("app").children) {
|
||||
if (el.id !== "success") el.classList.add("is-hidden");
|
||||
}
|
||||
document.getElementById('success').classList.remove('is-hidden');
|
||||
document.getElementById('success-detail').textContent =
|
||||
successName + ' — $' + (data.total || 0).toFixed(2) + ' total. You\'re all set!';
|
||||
const footer = document.querySelector('.sticky-footer');
|
||||
if (footer) footer.classList.add('is-hidden');
|
||||
try { localStorage.setItem('lastOrderWeek', WEEK); } catch (e) {}
|
||||
const successHeading = document.getElementById('success-heading');
|
||||
document.getElementById("success").classList.remove("is-hidden");
|
||||
document.getElementById("success-detail").textContent =
|
||||
successName +
|
||||
" — $" +
|
||||
(data.total || 0).toFixed(2) +
|
||||
" total. You're all set!";
|
||||
const footer = document.querySelector(".sticky-footer");
|
||||
if (footer) footer.classList.add("is-hidden");
|
||||
try {
|
||||
localStorage.setItem("lastOrderWeek", WEEK);
|
||||
} catch (e) {}
|
||||
const successHeading = document.getElementById("success-heading");
|
||||
if (successHeading) successHeading.focus();
|
||||
} else {
|
||||
showStatus(data.error || 'Something went wrong.', { tone: 'error', scope: 'form' });
|
||||
showStatus(data.error || "Something went wrong.", {
|
||||
tone: "error",
|
||||
scope: "form",
|
||||
});
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
btn.textContent = "Submit Order";
|
||||
}
|
||||
} catch (e) {
|
||||
showStatus('Failed to submit. Check your connection and try again.', { tone: 'error', scope: 'form' });
|
||||
showStatus("Failed to submit. Check your connection and try again.", {
|
||||
tone: "error",
|
||||
scope: "form",
|
||||
});
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
btn.textContent = "Submit Order";
|
||||
}
|
||||
}
|
||||
|
||||
function bindEmpNameChange(select) {
|
||||
if (select.dataset.boundChange === '1') return;
|
||||
select.dataset.boundChange = '1';
|
||||
select.addEventListener('change', () => {
|
||||
if (select.dataset.boundChange === "1") return;
|
||||
select.dataset.boundChange = "1";
|
||||
select.addEventListener("change", () => {
|
||||
const selected = select.options[select.selectedIndex];
|
||||
document.getElementById('emp-email').value = selected?.dataset?.email || '';
|
||||
clearStatus('form');
|
||||
document.getElementById("emp-email").value = selected?.dataset?.email || "";
|
||||
clearStatus("form");
|
||||
});
|
||||
}
|
||||
|
||||
function populateEmpSelect(select, employees) {
|
||||
select.innerHTML = '<option value="">Select your name</option>';
|
||||
employees.forEach(emp => {
|
||||
const opt = document.createElement('option');
|
||||
employees.forEach((emp) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = emp.name;
|
||||
opt.dataset.email = emp.email;
|
||||
opt.textContent = emp.name;
|
||||
|
|
@ -485,43 +535,55 @@ async function loadRoster() {
|
|||
const res = await fetch(ROSTER_URL);
|
||||
const data = await res.json();
|
||||
const candidate = data.employees;
|
||||
if (Array.isArray(candidate) && candidate.every(e => typeof e.name === 'string' && typeof e.email === 'string')) {
|
||||
if (
|
||||
Array.isArray(candidate) &&
|
||||
candidate.every(
|
||||
(e) => typeof e.name === "string" && typeof e.email === "string",
|
||||
)
|
||||
) {
|
||||
employees = candidate;
|
||||
} else {
|
||||
employees = ROSTER;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
const select = document.getElementById('emp-name');
|
||||
const select = document.getElementById("emp-name");
|
||||
populateEmpSelect(select, employees);
|
||||
}
|
||||
|
||||
function showClosedOverlay() {
|
||||
const overlay = document.getElementById('closed-overlay');
|
||||
overlay.classList.add('is-visible');
|
||||
overlay.style.display = 'flex';
|
||||
document.querySelectorAll('.qty-control button, .qty-control .qty').forEach(el => { el.disabled = true; });
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const overlay = document.getElementById("closed-overlay");
|
||||
overlay.classList.add("is-visible");
|
||||
overlay.style.display = "flex";
|
||||
document
|
||||
.querySelectorAll(".qty-control button, .qty-control .qty")
|
||||
.forEach((el) => {
|
||||
el.disabled = true;
|
||||
});
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
if (formStatusData) startCountdown(formStatusData);
|
||||
}
|
||||
|
||||
function adminBypassClosed() {
|
||||
const overlay = document.getElementById('closed-overlay');
|
||||
overlay.classList.remove('is-visible');
|
||||
overlay.style.display = 'none';
|
||||
document.querySelectorAll('.qty-control button').forEach(b => { b.disabled = false; });
|
||||
const submitBtn = document.getElementById('submit-btn');
|
||||
const overlay = document.getElementById("closed-overlay");
|
||||
overlay.classList.remove("is-visible");
|
||||
overlay.style.display = "none";
|
||||
document.querySelectorAll(".qty-control button").forEach((b) => {
|
||||
b.disabled = false;
|
||||
});
|
||||
const submitBtn = document.getElementById("submit-btn");
|
||||
if (submitBtn) submitBtn.disabled = Object.keys(quantities).length === 0;
|
||||
if (!document.getElementById('admin-closed-banner')) {
|
||||
const banner = document.createElement('div');
|
||||
banner.id = 'admin-closed-banner';
|
||||
banner.className = 'closed-banner';
|
||||
banner.textContent = 'Form is closed — admin override active';
|
||||
const deadline = document.querySelector('.deadline');
|
||||
if (deadline) deadline.parentNode.insertBefore(banner, deadline.nextSibling);
|
||||
if (!document.getElementById("admin-closed-banner")) {
|
||||
const banner = document.createElement("div");
|
||||
banner.id = "admin-closed-banner";
|
||||
banner.className = "closed-banner";
|
||||
banner.textContent = "Form is closed — admin override active";
|
||||
const deadline = document.querySelector(".deadline");
|
||||
if (deadline)
|
||||
deadline.parentNode.insertBefore(banner, deadline.nextSibling);
|
||||
}
|
||||
document.querySelectorAll('[data-meal-index]').forEach(card => {
|
||||
document.querySelectorAll("[data-meal-index]").forEach((card) => {
|
||||
const index = parseInt(card.dataset.mealIndex, 10);
|
||||
const meal = MEALS[index];
|
||||
if (meal) updateMealCard(card, meal, index);
|
||||
|
|
@ -532,17 +594,17 @@ function adminBypassClosed() {
|
|||
function checkAdmin() {
|
||||
if (!ADMIN_URL) return Promise.resolve();
|
||||
const headers = {};
|
||||
if (googleCredential) headers['Authorization'] = 'Bearer ' + googleCredential;
|
||||
if (googleCredential) headers["Authorization"] = "Bearer " + googleCredential;
|
||||
return fetch(ADMIN_URL, { headers })
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => {
|
||||
if (data && data.weeks) {
|
||||
isAdmin = true;
|
||||
window.isAdmin = true;
|
||||
adminWeeks = data.weeks;
|
||||
window.adminWeeks = adminWeeks;
|
||||
const ab = document.getElementById('admin-btn');
|
||||
if (ab) ab.classList.remove('is-hidden');
|
||||
const ab = document.getElementById("admin-btn");
|
||||
if (ab) ab.classList.remove("is-hidden");
|
||||
if (formClosed) adminBypassClosed();
|
||||
} else if (formClosed) {
|
||||
showClosedOverlay();
|
||||
|
|
@ -554,62 +616,71 @@ function checkAdmin() {
|
|||
}
|
||||
|
||||
function showAdmin() {
|
||||
document.querySelectorAll('#app > :not(#admin-panel)').forEach(el => {
|
||||
if (el.id !== 'admin-panel') {
|
||||
el.dataset.prevHidden = el.classList.contains('is-hidden') ? '1' : '';
|
||||
el.classList.add('is-hidden');
|
||||
document.querySelectorAll("#app > :not(#admin-panel)").forEach((el) => {
|
||||
if (el.id !== "admin-panel") {
|
||||
el.dataset.prevHidden = el.classList.contains("is-hidden") ? "1" : "";
|
||||
el.classList.add("is-hidden");
|
||||
}
|
||||
});
|
||||
document.getElementById('admin-panel').classList.remove('is-hidden');
|
||||
const footer = document.querySelector('.sticky-footer');
|
||||
if (footer) footer.classList.add('is-hidden');
|
||||
document.getElementById("admin-panel").classList.remove("is-hidden");
|
||||
const footer = document.querySelector(".sticky-footer");
|
||||
if (footer) footer.classList.add("is-hidden");
|
||||
|
||||
const select = document.getElementById('admin-week');
|
||||
select.innerHTML = '';
|
||||
adminWeeks.forEach(w => {
|
||||
const opt = document.createElement('option');
|
||||
const select = document.getElementById("admin-week");
|
||||
select.innerHTML = "";
|
||||
adminWeeks.forEach((w) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = w.week;
|
||||
opt.textContent = w.week + (w.form_status === 'open' ? ' (open)' : '');
|
||||
opt.textContent = w.week + (w.form_status === "open" ? " (open)" : "");
|
||||
select.appendChild(opt);
|
||||
});
|
||||
if (adminWeeks.length > 0 && typeof loadWeekOrders === 'function') {
|
||||
if (adminWeeks.length > 0 && typeof loadWeekOrders === "function") {
|
||||
loadWeekOrders(adminWeeks[0].week);
|
||||
}
|
||||
}
|
||||
|
||||
function hideAdmin() {
|
||||
document.getElementById('admin-panel').classList.add('is-hidden');
|
||||
document.querySelectorAll('#app > :not(#admin-panel)').forEach(el => {
|
||||
if (el.dataset.prevHidden === '1') el.classList.add('is-hidden');
|
||||
else el.classList.remove('is-hidden');
|
||||
document.getElementById("admin-panel").classList.add("is-hidden");
|
||||
document.querySelectorAll("#app > :not(#admin-panel)").forEach((el) => {
|
||||
if (el.dataset.prevHidden === "1") el.classList.add("is-hidden");
|
||||
else el.classList.remove("is-hidden");
|
||||
delete el.dataset.prevHidden;
|
||||
});
|
||||
const footer = document.querySelector('.sticky-footer');
|
||||
if (footer) footer.classList.remove('is-hidden');
|
||||
const footer = document.querySelector(".sticky-footer");
|
||||
if (footer) footer.classList.remove("is-hidden");
|
||||
}
|
||||
|
||||
const easternFormatter = new Intl.DateTimeFormat('en-US', {
|
||||
timeZone: 'America/New_York',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
const easternFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: "America/New_York",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: "h23",
|
||||
});
|
||||
|
||||
function getEasternParts(date) {
|
||||
const values = {};
|
||||
easternFormatter.formatToParts(date).forEach(part => {
|
||||
if (part.type !== 'literal') values[part.type] = Number(part.value);
|
||||
easternFormatter.formatToParts(date).forEach((part) => {
|
||||
if (part.type !== "literal") values[part.type] = Number(part.value);
|
||||
});
|
||||
return values;
|
||||
}
|
||||
|
||||
function getEasternOffsetMs(date) {
|
||||
const parts = getEasternParts(date);
|
||||
return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second) - date.getTime();
|
||||
return (
|
||||
Date.UTC(
|
||||
parts.year,
|
||||
parts.month - 1,
|
||||
parts.day,
|
||||
parts.hour,
|
||||
parts.minute,
|
||||
parts.second,
|
||||
) - date.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
function startCountdown(data) {
|
||||
|
|
@ -627,24 +698,36 @@ function updateCountdown() {
|
|||
target = new Date(reopenAtMs);
|
||||
} else {
|
||||
const etNow = getEasternParts(now);
|
||||
const day = new Date(Date.UTC(etNow.year, etNow.month - 1, etNow.day)).getUTCDay();
|
||||
const day = new Date(
|
||||
Date.UTC(etNow.year, etNow.month - 1, etNow.day),
|
||||
).getUTCDay();
|
||||
const hour = etNow.hour;
|
||||
|
||||
let daysUntil = (1 - day + 7) % 7;
|
||||
if (daysUntil === 0 && hour >= 8) daysUntil = 7;
|
||||
|
||||
const targetEtMs = Date.UTC(etNow.year, etNow.month - 1, etNow.day + daysUntil, 8, 0, 0);
|
||||
const targetEtMs = Date.UTC(
|
||||
etNow.year,
|
||||
etNow.month - 1,
|
||||
etNow.day + daysUntil,
|
||||
8,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
target = new Date(targetEtMs - getEasternOffsetMs(new Date(targetEtMs)));
|
||||
}
|
||||
|
||||
let diff = Math.max(0, Math.floor((target - now) / 1000));
|
||||
const d = Math.floor(diff / 86400); diff %= 86400;
|
||||
const h = Math.floor(diff / 3600); diff %= 3600;
|
||||
const d = Math.floor(diff / 86400);
|
||||
diff %= 86400;
|
||||
const h = Math.floor(diff / 3600);
|
||||
diff %= 3600;
|
||||
const m = Math.floor(diff / 60);
|
||||
const s = diff % 60;
|
||||
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
document.getElementById('countdown').textContent = d + 'd ' + pad(h) + 'h ' + pad(m) + 'm ' + pad(s) + 's';
|
||||
const pad = (n) => String(n).padStart(2, "0");
|
||||
document.getElementById("countdown").textContent =
|
||||
d + "d " + pad(h) + "h " + pad(m) + "m " + pad(s) + "s";
|
||||
}
|
||||
|
||||
async function checkFormStatus() {
|
||||
|
|
@ -652,12 +735,12 @@ async function checkFormStatus() {
|
|||
try {
|
||||
const res = await fetch(STATUS_URL);
|
||||
const data = await res.json();
|
||||
if (data.status === 'closed') {
|
||||
if (data.status === "closed") {
|
||||
formClosed = true;
|
||||
formStatusData = data;
|
||||
if (isAdmin) {
|
||||
adminBypassClosed();
|
||||
} else if (CONFIG.authMode !== 'google' || googleCredential) {
|
||||
} else if (CONFIG.authMode !== "google" || googleCredential) {
|
||||
showClosedOverlay();
|
||||
}
|
||||
}
|
||||
|
|
@ -666,7 +749,7 @@ async function checkFormStatus() {
|
|||
|
||||
function init() {
|
||||
checkFormStatus();
|
||||
if (CONFIG.authMode === 'google') {
|
||||
if (CONFIG.authMode === "google") {
|
||||
waitForGoogleAuth();
|
||||
} else {
|
||||
loadRoster();
|
||||
|
|
@ -674,39 +757,40 @@ function init() {
|
|||
}
|
||||
|
||||
const tags = new Set();
|
||||
MEALS.forEach(m => (m.dietary_tags || []).forEach(t => tags.add(t)));
|
||||
const cats = ['All Meals', ...Array.from(tags).sort()];
|
||||
const filtersEl = document.getElementById('filters');
|
||||
cats.forEach(cat => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
const isActive = cat === 'All Meals';
|
||||
btn.className = 'btn btn-chip' + (isActive ? ' active' : '');
|
||||
btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
MEALS.forEach((m) => (m.dietary_tags || []).forEach((t) => tags.add(t)));
|
||||
const cats = ["All Meals", ...Array.from(tags).sort()];
|
||||
const filtersEl = document.getElementById("filters");
|
||||
cats.forEach((cat) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
const isActive = cat === "All Meals";
|
||||
btn.className = "btn btn-chip" + (isActive ? " active" : "");
|
||||
btn.setAttribute("aria-pressed", isActive ? "true" : "false");
|
||||
btn.textContent = cat;
|
||||
btn.addEventListener('click', () => {
|
||||
filtersEl.querySelectorAll('button').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.setAttribute('aria-pressed', 'false');
|
||||
btn.addEventListener("click", () => {
|
||||
filtersEl.querySelectorAll("button").forEach((b) => {
|
||||
b.classList.remove("active");
|
||||
b.setAttribute("aria-pressed", "false");
|
||||
});
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
btn.classList.add("active");
|
||||
btn.setAttribute("aria-pressed", "true");
|
||||
renderMeals();
|
||||
});
|
||||
filtersEl.appendChild(btn);
|
||||
});
|
||||
|
||||
document.getElementById('search').addEventListener('input', renderMeals);
|
||||
document.getElementById('submit-btn').addEventListener('click', submitOrder);
|
||||
document.getElementById("search").addEventListener("input", renderMeals);
|
||||
document.getElementById("submit-btn").addEventListener("click", submitOrder);
|
||||
|
||||
const adminBtn = document.getElementById('admin-btn');
|
||||
if (adminBtn) adminBtn.addEventListener('click', showAdmin);
|
||||
const adminBtn = document.getElementById("admin-btn");
|
||||
if (adminBtn) adminBtn.addEventListener("click", showAdmin);
|
||||
|
||||
const signOutBtn = document.getElementById('sign-out-btn');
|
||||
if (signOutBtn) signOutBtn.addEventListener('click', signOut);
|
||||
const signOutBtn = document.getElementById("sign-out-btn");
|
||||
if (signOutBtn) signOutBtn.addEventListener("click", signOut);
|
||||
|
||||
const successBackBtn = document.getElementById('success-back-btn');
|
||||
if (successBackBtn) successBackBtn.addEventListener('click', () => location.reload());
|
||||
const successBackBtn = document.getElementById("success-back-btn");
|
||||
if (successBackBtn)
|
||||
successBackBtn.addEventListener("click", () => location.reload());
|
||||
|
||||
bindMealsListDelegation();
|
||||
renderMeals();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue