diff --git a/.history/package-lock_20251221182740.json b/.history/package-lock_20251221182740.json new file mode 100644 index 0000000..eb465ef --- /dev/null +++ b/.history/package-lock_20251221182740.json @@ -0,0 +1,17590 @@ +{ + "name": "schedulr", +<<<<<<< HEAD + "version": "1.0.3", +======= + "version": "1.1.2", +>>>>>>> origin/main + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "schedulr", +<<<<<<< HEAD + "version": "1.0.3", +======= + "version": "1.1.2", +>>>>>>> origin/main + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.4.6", + "clsx": "^2.1.1", + "electron-squirrel-startup": "^1.0.1", + "i18next": "^25.6.3", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.554.0", + "papaparse": "^5.5.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", + "react-is": "^19.2.0", + "react-router-dom": "^7.9.6", + "recharts": "^3.4.1", + "tailwind-merge": "^3.4.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@electron-forge/cli": "^7.10.2", + "@electron-forge/maker-deb": "^7.10.2", + "@electron-forge/maker-dmg": "^7.10.2", + "@electron-forge/maker-rpm": "^7.10.2", + "@electron-forge/maker-squirrel": "^7.10.2", + "@electron-forge/maker-zip": "^7.10.2", + "@electron-forge/plugin-auto-unpack-natives": "^7.10.2", + "@electron-forge/plugin-fuses": "^7.10.2", + "@electron-forge/plugin-vite": "^7.10.2", + "@electron/fuses": "^1.8.0", + "@tailwindcss/postcss": "^4.1.17", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/better-sqlite3": "^7.6.13", + "@types/electron-squirrel-startup": "^1.0.2", + "@types/jest": "^30.0.0", + "@types/mocha": "^10.0.10", + "@types/papaparse": "^5.5.0", + "@types/react": "^19.2.6", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "electron": "39.2.3", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.32.0", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "ts-jest": "^29.4.6", + "typescript": "^5.9.3", + "vite": "^5.4.21" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron-forge/cli": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz", + "integrity": "sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-cli?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "MIT", + "dependencies": { + "@electron-forge/core": "7.10.2", + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@electron/get": "^3.0.0", + "@inquirer/prompts": "^6.0.1", + "@listr2/prompt-adapter-inquirer": "^2.0.22", + "chalk": "^4.0.0", + "commander": "^11.1.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "listr2": "^7.0.2", + "log-symbols": "^4.0.0", + "semver": "^7.2.1" + }, + "bin": { + "electron-forge": "dist/electron-forge.js", + "electron-forge-vscode-nix": "script/vscode.sh", + "electron-forge-vscode-win": "script/vscode.cmd" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/core": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.10.2.tgz", + "integrity": "sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-core?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "MIT", + "dependencies": { + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/publisher-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "@electron-forge/template-vite": "7.10.2", + "@electron-forge/template-vite-typescript": "7.10.2", + "@electron-forge/template-webpack": "7.10.2", + "@electron-forge/template-webpack-typescript": "7.10.2", + "@electron-forge/tracer": "7.10.2", + "@electron/get": "^3.0.0", + "@electron/packager": "^18.3.5", + "@electron/rebuild": "^3.7.0", + "@malept/cross-spawn-promise": "^2.0.0", + "@vscode/sudo-prompt": "^9.3.1", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "fast-glob": "^3.2.7", + "filenamify": "^4.1.0", + "find-up": "^5.0.0", + "fs-extra": "^10.0.0", + "global-dirs": "^3.0.0", + "got": "^11.8.5", + "interpret": "^3.1.1", + "jiti": "^2.4.2", + "listr2": "^7.0.2", + "lodash": "^4.17.20", + "log-symbols": "^4.0.0", + "node-fetch": "^2.6.7", + "rechoir": "^0.8.0", + "semver": "^7.2.1", + "source-map-support": "^0.5.13", + "username": "^5.1.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/core-utils": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.10.2.tgz", + "integrity": "sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron/rebuild": "^3.7.0", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "find-up": "^5.0.0", + "fs-extra": "^10.0.0", + "log-symbols": "^4.0.0", + "parse-author": "^2.0.0", + "semver": "^7.2.1" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/maker-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.10.2.tgz", + "integrity": "sha512-1QN4qnPVTjo+qWYG+s0kYv7XcuIowsPVvbl718FgJUcvkxyRjUA6kWHjFxRvdV6g7Sa2PzZBF+/Mrjpws1lehQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/maker-deb": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-deb/-/maker-deb-7.10.2.tgz", + "integrity": "sha512-4MPr9NW5UbEUbf9geZn5R/0O/QVIiy2EgUXOYOeKkA7oR8U6I1I3+BytYFHYcxbY6+PGhi1H1VTLJLITbHGVWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-debian": "^3.2.0" + } + }, + "node_modules/@electron-forge/maker-dmg": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-dmg/-/maker-dmg-7.10.2.tgz", + "integrity": "sha512-ksSX6/Ioxa3h3rEGIg26qfDcJgB3aFGivitRdSkEnzUCLWJSUoThEwLToA7CAq4J/4ZREK0PDJ7FPsB+F8CYfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-dmg": "^5.0.1" + } + }, + "node_modules/@electron-forge/maker-rpm": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-rpm/-/maker-rpm-7.10.2.tgz", + "integrity": "sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-redhat": "^3.2.0" + } + }, + "node_modules/@electron-forge/maker-squirrel": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.10.2.tgz", + "integrity": "sha512-Y5EhNSBXf4a7qcq+BK/x5qVDlQ1Gez5V+arUpDvVxf1zwvsB1aSyAjmoBrOKGYD9A5pJzjkMWMDw95MStl1W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-winstaller": "^5.3.0" + } + }, + "node_modules/@electron-forge/maker-zip": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-7.10.2.tgz", + "integrity": "sha512-APRqVPM+O1rj4O7sk5f8tqJpS5UgxcUJEsCnXN4JRpdRvsOlMopzYZdazlCLH9l7S+r4ZKirjtMluIGeYq8YOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "cross-zip": "^4.0.0", + "fs-extra": "^10.0.0", + "got": "^11.8.5" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-auto-unpack-natives": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.10.2.tgz", + "integrity": "sha512-uQnahm1DECwqI8hBC7PKccyfovY/YqHNz8de3OxyjQDmwsqQfCA8Ucyh1E9n4NMEpw6Co8KLn+qF2BuIOsftLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.10.2.tgz", + "integrity": "sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-fuses": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz", + "integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "peerDependencies": { + "@electron/fuses": "^1.0.0" + } + }, + "node_modules/@electron-forge/plugin-vite": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-vite/-/plugin-vite-7.10.2.tgz", + "integrity": "sha512-aHotwaVlbSwVDb+Z+JdU6cMYhestt8ncmXKv4Uwm7of/gWAdvS7o/ohQVWkjXhzSidriCTwFMRz4jELJbnkNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "listr2": "^7.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/publisher-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.10.2.tgz", + "integrity": "sha512-2k2VOY0wOoAgQoQXn/u3EJ2Ka2v363+wC/+zUMTWGeRHW8pRwX84WX2SpsTttRzbsqAEMJYw5FAzgMBEQUTfpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/shared-types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.10.2.tgz", + "integrity": "sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/tracer": "7.10.2", + "@electron/packager": "^18.3.5", + "@electron/rebuild": "^3.7.0", + "listr2": "^7.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.10.2.tgz", + "integrity": "sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "semver": "^7.2.1", + "username": "^5.1.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-vite": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.10.2.tgz", + "integrity": "sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-vite-typescript": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.2.tgz", + "integrity": "sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.10.2.tgz", + "integrity": "sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack-typescript": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.2.tgz", + "integrity": "sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0", + "typescript": "~5.4.5", + "webpack": "^5.69.1" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack-typescript/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@electron-forge/tracer": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz", + "integrity": "sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chrome-trace-event": "^1.0.3" + }, + "engines": { + "node": ">= 14.17.5" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/packager": { + "version": "18.4.4", + "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-18.4.4.tgz", + "integrity": "sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/asar": "^3.2.13", + "@electron/get": "^3.0.0", + "@electron/notarize": "^2.1.0", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^2.0.1", + "@electron/windows-sign": "^1.0.0", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "prettier": "^3.4.2", + "resedit": "^2.0.0", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 16.13.0" + }, + "funding": { + "url": "https://github.com/electron/packager?sponsor=1" + } + }, + "node_modules/@electron/packager/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz", + "integrity": "sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", + "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", + "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@types/appdmg": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@types/appdmg/-/appdmg-0.5.5.tgz", + "integrity": "sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/electron-squirrel-startup": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz", + "integrity": "sha512-AzxnvBzNh8K/0SmxMmZtpJf1/IWoGXLP+pQDuUaVkPyotI8ryvAtBSqgxR/qOSvxWHYWrxkeNsJ+Ca5xOuUxJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/papaparse": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.1.tgz", + "integrity": "sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vscode/sudo-prompt": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz", + "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdmg": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz", + "integrity": "sha512-GRmFKlCG+PWbcYF4LUNonTYmy0GjguDy6Jh9WP8mpd0T6j80XIJyXBiWlD0U+MLNhqV9Nhx49Gl9GpVToulpLg==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "async": "^1.4.2", + "ds-store": "^0.1.5", + "execa": "^1.0.0", + "fs-temp": "^1.0.0", + "fs-xattr": "^0.3.0", + "image-size": "^0.7.4", + "is-my-json-valid": "^2.20.0", + "minimist": "^1.1.3", + "parse-color": "^1.0.0", + "path-exists": "^4.0.0", + "repeat-string": "^1.5.4" + }, + "bin": { + "appdmg": "bin/appdmg.js" + }, + "engines": { + "node": ">=8.5" + } + }, + "node_modules/appdmg/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/appdmg/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/appdmg/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/appdmg/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/appdmg/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/appdmg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/appdmg/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/appdmg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base32-encode": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", + "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-data-view": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/bplist-creator": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", + "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "stream-buffers": "~2.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT" + }, + "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/cross-zip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cross-zip/-/cross-zip-4.0.1.tgz", + "integrity": "sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.10" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ds-store": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", + "integrity": "sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bplist-creator": "~0.0.3", + "macos-alias": "~0.2.5", + "tn1150": "^0.1.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron": { + "version": "39.2.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz", + "integrity": "sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-installer-common": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.4.tgz", + "integrity": "sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@electron/asar": "^3.2.5", + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "glob": "^7.1.4", + "lodash": "^4.17.15", + "parse-author": "^2.0.0", + "semver": "^7.1.1", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "url": "https://github.com/electron-userland/electron-installer-common?sponsor=1" + }, + "optionalDependencies": { + "@types/fs-extra": "^9.0.1" + } + }, + "node_modules/electron-installer-common/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-common/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-3.2.0.tgz", + "integrity": "sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.2", + "fs-extra": "^9.0.0", + "get-folder-size": "^2.0.1", + "lodash": "^4.17.4", + "word-wrap": "^1.2.3", + "yargs": "^16.0.2" + }, + "bin": { + "electron-installer-debian": "src/cli.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-installer-debian/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-debian/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/electron-installer-debian/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron-installer-debian/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-debian/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-debian/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-installer-debian/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-dmg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/electron-installer-dmg/-/electron-installer-dmg-5.0.1.tgz", + "integrity": "sha512-qOa1aAQdX57C+vzhDk3549dd/PRlNL4F8y736MTD1a43qptD+PvHY97Bo9gSf+OZ8iUWE7BrYSpk/FgLUe40EA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/appdmg": "^0.5.5", + "debug": "^4.3.2", + "minimist": "^1.2.7" + }, + "bin": { + "electron-installer-dmg": "dist/electron-installer-dmg-bin.js" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "appdmg": "^0.6.4" + } + }, + "node_modules/electron-installer-redhat": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/electron-installer-redhat/-/electron-installer-redhat-3.4.0.tgz", + "integrity": "sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.2", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "word-wrap": "^1.2.3", + "yargs": "^16.0.2" + }, + "bin": { + "electron-installer-redhat": "src/cli.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-installer-redhat/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-redhat/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/electron-installer-redhat/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron-installer-redhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-redhat/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-redhat/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-redhat/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-installer-redhat/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-redhat/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-squirrel-startup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", + "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/electron/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/electron/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/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/esrecurse/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/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "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/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fmix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", + "integrity": "sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "imul": "^1.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-temp": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", + "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "random-path": "^0.1.0" + } + }, + "node_modules/fs-xattr": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", + "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/gar": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gar/-/gar-1.0.4.tgz", + "integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-property": "^1.0.0" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-folder-size": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz", + "integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "gar": "^1.0.4", + "tiny-each-async": "2.0.3" + }, + "bin": { + "get-folder-size": "bin/get-folder-size" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/i18next": { + "version": "25.7.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz", + "integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/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/image-size": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imul": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", + "integrity": "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-my-ip-valid": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-my-json-valid": { + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", + "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.554.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz", + "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/macos-alias": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.12.tgz", + "integrity": "sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "nan": "^2.4.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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/murmur-32": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", + "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "encode-utf8": "^1.0.3", + "fmix": "^0.1.0", + "imul": "^1.0.0" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", + "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "~0.5.0" + } + }, + "node_modules/parse-color/node_modules/color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==", + "dev": true, + "optional": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz", + "integrity": "sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/random-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", + "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "base32-encode": "^0.1.0 || ^1.0.0", + "murmur-32": "^0.1.0 || ^0.2.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-i18next": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.4.1.tgz", + "integrity": "sha512-GzsYomxb1/uE7nlJm0e1qQ8f+W9I3Xirh9VoycZIahk6C8Pmv/9Fd0ek6zjf1FSgtGLElDGqwi/4FOHEGUbsEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", + "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz", + "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==", + "license": "MIT", + "dependencies": { + "react-router": "7.10.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.1.tgz", + "integrity": "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-2.0.3.tgz", + "integrity": "sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^1.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "optional": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-each-async": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", + "integrity": "sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp-promise/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tn1150": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", + "integrity": "sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "unorm": "^1.4.1" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "dev": true, + "license": "MIT or GPL-2.0", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/username": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", + "integrity": "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0", + "mem": "^4.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/username/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/username/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/username/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/username/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.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/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.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" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/.history/package-lock_20251221183147.json b/.history/package-lock_20251221183147.json new file mode 100644 index 0000000..d18c764 --- /dev/null +++ b/.history/package-lock_20251221183147.json @@ -0,0 +1,17582 @@ +{ + "name": "schedulr", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "schedulr", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "better-sqlite3": "^12.4.6", + "clsx": "^2.1.1", + "electron-squirrel-startup": "^1.0.1", + "i18next": "^25.6.3", + "i18next-browser-languagedetector": "^8.2.0", + "lucide-react": "^0.554.0", + "papaparse": "^5.5.3", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-i18next": "^16.3.5", + "react-is": "^19.2.0", + "react-router-dom": "^7.9.6", + "recharts": "^3.4.1", + "tailwind-merge": "^3.4.0", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@electron-forge/cli": "^7.10.2", + "@electron-forge/maker-deb": "^7.10.2", + "@electron-forge/maker-dmg": "^7.10.2", + "@electron-forge/maker-rpm": "^7.10.2", + "@electron-forge/maker-squirrel": "^7.10.2", + "@electron-forge/maker-zip": "^7.10.2", + "@electron-forge/plugin-auto-unpack-natives": "^7.10.2", + "@electron-forge/plugin-fuses": "^7.10.2", + "@electron-forge/plugin-vite": "^7.10.2", + "@electron/fuses": "^1.8.0", + "@tailwindcss/postcss": "^4.1.17", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/better-sqlite3": "^7.6.13", + "@types/electron-squirrel-startup": "^1.0.2", + "@types/jest": "^30.0.0", + "@types/mocha": "^10.0.10", + "@types/papaparse": "^5.5.0", + "@types/react": "^19.2.6", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.22", + "electron": "39.2.3", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.32.0", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.17", + "ts-jest": "^29.4.6", + "typescript": "^5.9.3", + "vite": "^5.4.21" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@electron-forge/cli": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/cli/-/cli-7.10.2.tgz", + "integrity": "sha512-X1RtS5IqNgzGDS2rr1q0Y74wU/m3DbU4vSgllNun1ZQv1BfMpDcKLhnKi3aeetoA0huLTpMVU9eWJ7bziI9fxA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-cli?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "MIT", + "dependencies": { + "@electron-forge/core": "7.10.2", + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@electron/get": "^3.0.0", + "@inquirer/prompts": "^6.0.1", + "@listr2/prompt-adapter-inquirer": "^2.0.22", + "chalk": "^4.0.0", + "commander": "^11.1.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "listr2": "^7.0.2", + "log-symbols": "^4.0.0", + "semver": "^7.2.1" + }, + "bin": { + "electron-forge": "dist/electron-forge.js", + "electron-forge-vscode-nix": "script/vscode.sh", + "electron-forge-vscode-win": "script/vscode.cmd" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/core": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/core/-/core-7.10.2.tgz", + "integrity": "sha512-HAIuOtpOfGjA0cd55tbEV2gAv+A7tSZg9bonmVDYFEe6dBgbLk8a3+/1fJUdWW8fyFkg1wa8zK7pjP751bAXsA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.electron-forge-core?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "MIT", + "dependencies": { + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/publisher-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "@electron-forge/template-vite": "7.10.2", + "@electron-forge/template-vite-typescript": "7.10.2", + "@electron-forge/template-webpack": "7.10.2", + "@electron-forge/template-webpack-typescript": "7.10.2", + "@electron-forge/tracer": "7.10.2", + "@electron/get": "^3.0.0", + "@electron/packager": "^18.3.5", + "@electron/rebuild": "^3.7.0", + "@malept/cross-spawn-promise": "^2.0.0", + "@vscode/sudo-prompt": "^9.3.1", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "fast-glob": "^3.2.7", + "filenamify": "^4.1.0", + "find-up": "^5.0.0", + "fs-extra": "^10.0.0", + "global-dirs": "^3.0.0", + "got": "^11.8.5", + "interpret": "^3.1.1", + "jiti": "^2.4.2", + "listr2": "^7.0.2", + "lodash": "^4.17.20", + "log-symbols": "^4.0.0", + "node-fetch": "^2.6.7", + "rechoir": "^0.8.0", + "semver": "^7.2.1", + "source-map-support": "^0.5.13", + "username": "^5.1.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/core-utils": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/core-utils/-/core-utils-7.10.2.tgz", + "integrity": "sha512-JXrk2hWR4q8KgZFABpojjuqql3tYeVIH6qmtbkNEkZEQq7YIxajJBCct7J7bWfNQTmHotsQ3k5KLknhyhTaBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron/rebuild": "^3.7.0", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "find-up": "^5.0.0", + "fs-extra": "^10.0.0", + "log-symbols": "^4.0.0", + "parse-author": "^2.0.0", + "semver": "^7.2.1" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/maker-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-base/-/maker-base-7.10.2.tgz", + "integrity": "sha512-1QN4qnPVTjo+qWYG+s0kYv7XcuIowsPVvbl718FgJUcvkxyRjUA6kWHjFxRvdV6g7Sa2PzZBF+/Mrjpws1lehQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0", + "which": "^2.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/maker-deb": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-deb/-/maker-deb-7.10.2.tgz", + "integrity": "sha512-4MPr9NW5UbEUbf9geZn5R/0O/QVIiy2EgUXOYOeKkA7oR8U6I1I3+BytYFHYcxbY6+PGhi1H1VTLJLITbHGVWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-debian": "^3.2.0" + } + }, + "node_modules/@electron-forge/maker-dmg": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-dmg/-/maker-dmg-7.10.2.tgz", + "integrity": "sha512-ksSX6/Ioxa3h3rEGIg26qfDcJgB3aFGivitRdSkEnzUCLWJSUoThEwLToA7CAq4J/4ZREK0PDJ7FPsB+F8CYfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-dmg": "^5.0.1" + } + }, + "node_modules/@electron-forge/maker-rpm": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-rpm/-/maker-rpm-7.10.2.tgz", + "integrity": "sha512-LQoeYzbY/z1yuBBA+bNutCJmhCA4NcXUbFO4OTqsIX8B6y1zNTYZT4JEuhoK7eBsP4/Rz6u/JnNp0XOyjftOUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-installer-redhat": "^3.2.0" + } + }, + "node_modules/@electron-forge/maker-squirrel": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.10.2.tgz", + "integrity": "sha512-Y5EhNSBXf4a7qcq+BK/x5qVDlQ1Gez5V+arUpDvVxf1zwvsB1aSyAjmoBrOKGYD9A5pJzjkMWMDw95MStl1W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + }, + "optionalDependencies": { + "electron-winstaller": "^5.3.0" + } + }, + "node_modules/@electron-forge/maker-zip": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/maker-zip/-/maker-zip-7.10.2.tgz", + "integrity": "sha512-APRqVPM+O1rj4O7sk5f8tqJpS5UgxcUJEsCnXN4JRpdRvsOlMopzYZdazlCLH9l7S+r4ZKirjtMluIGeYq8YOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/maker-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "cross-zip": "^4.0.0", + "fs-extra": "^10.0.0", + "got": "^11.8.5" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-auto-unpack-natives": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-auto-unpack-natives/-/plugin-auto-unpack-natives-7.10.2.tgz", + "integrity": "sha512-uQnahm1DECwqI8hBC7PKccyfovY/YqHNz8de3OxyjQDmwsqQfCA8Ucyh1E9n4NMEpw6Co8KLn+qF2BuIOsftLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-base/-/plugin-base-7.10.2.tgz", + "integrity": "sha512-+4YLmkLZxvS6JFXYNI4dHt8Il8iIvwk2o6lCJGwNysOUq2KOZ3Wu1He4Ko8HhKcO1VWbFvslbh57oQn963Aryw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/plugin-fuses": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-fuses/-/plugin-fuses-7.10.2.tgz", + "integrity": "sha512-X8FaBL5pVvKCTBNaa9EjbH6vuaeIU7UcPSmP9501XF4zcKPCfTbQKz49LTMl7gd5YzUm82IlqRjte12LLpcSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + }, + "peerDependencies": { + "@electron/fuses": "^1.0.0" + } + }, + "node_modules/@electron-forge/plugin-vite": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/plugin-vite/-/plugin-vite-7.10.2.tgz", + "integrity": "sha512-aHotwaVlbSwVDb+Z+JdU6cMYhestt8ncmXKv4Uwm7of/gWAdvS7o/ohQVWkjXhzSidriCTwFMRz4jELJbnkNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/plugin-base": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "chalk": "^4.0.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "listr2": "^7.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/publisher-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-7.10.2.tgz", + "integrity": "sha512-2k2VOY0wOoAgQoQXn/u3EJ2Ka2v363+wC/+zUMTWGeRHW8pRwX84WX2SpsTttRzbsqAEMJYw5FAzgMBEQUTfpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/shared-types": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/shared-types/-/shared-types-7.10.2.tgz", + "integrity": "sha512-e2pd9RsdbKwsNf6UtKoolmJGy92Nc0/XO4SI91doV8cM954hM2XSYz3VHoqXebMFAF1JDfXoEUt6UCRbEDgMgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/tracer": "7.10.2", + "@electron/packager": "^18.3.5", + "@electron/rebuild": "^3.7.0", + "listr2": "^7.0.2" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-base": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-base/-/template-base-7.10.2.tgz", + "integrity": "sha512-D9DbEx3rtikIhUyn4tcz2pJqHNU/+FXKNnzSvmrJoJ9LusR3C42OU9GtbU8oT3nawpnCGgPFIOGXrzexFPp6DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/core-utils": "7.10.2", + "@electron-forge/shared-types": "7.10.2", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "fs-extra": "^10.0.0", + "semver": "^7.2.1", + "username": "^5.1.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-vite": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-vite/-/template-vite-7.10.2.tgz", + "integrity": "sha512-hR9HBOM902yq7zhFl8bO3w5ufMgitdd5ZwDzAdKITFh2ttZemHy9ha5S0K+R+4GoXHz8t7hUTHk8+iPy09qrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-vite-typescript": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-vite-typescript/-/template-vite-typescript-7.10.2.tgz", + "integrity": "sha512-df7rpxxIOIyZn0RfQ1GIlLW7dXhxkerc9uZ3ozO4C7zfvip3z0Mg+wS1synktPfr4WISaPktIdnj3mVu6Uu7Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack/-/template-webpack-7.10.2.tgz", + "integrity": "sha512-VIUXA+XHM5SLjg7fIpOOmBsgi0LstkjrEz4gUzVL0AaITM7e+BCziIHld1ceXLbQ1FnKtrUGnQ9X/cHYxYvhHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack-typescript": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/template-webpack-typescript/-/template-webpack-typescript-7.10.2.tgz", + "integrity": "sha512-JtrLUAFbxxWJ1kU7b8MNyL5SO9/rY5UeNz1b9hvMvilW8GxyMWUen58dafgdnx3OpKLNZnhOOhgRagNppEzJOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron-forge/shared-types": "7.10.2", + "@electron-forge/template-base": "7.10.2", + "fs-extra": "^10.0.0", + "typescript": "~5.4.5", + "webpack": "^5.69.1" + }, + "engines": { + "node": ">= 16.4.0" + } + }, + "node_modules/@electron-forge/template-webpack-typescript/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@electron-forge/tracer": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@electron-forge/tracer/-/tracer-7.10.2.tgz", + "integrity": "sha512-jhLLQbttfZViSOYn/3SJc8HML+jNZAytPVJwgGGd3coUiFysWJ2Xald99iqOiouPAhIigBfNPxQb/q/EbcDu4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chrome-trace-event": "^1.0.3" + }, + "engines": { + "node": ">= 14.17.5" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@electron/get/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@electron/get/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@electron/node-gyp": { + "version": "10.2.0-electron.1", + "resolved": "git+ssh://git@github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "integrity": "sha512-CrYo6TntjpoMO1SHjl5Pa/JoUsECNqNdB7Kx49WLQpWzPw53eEITJ2Hs9fh/ryUYDn4pxZz11StaBYBrLFJdqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^8.1.0", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.2.1", + "nopt": "^6.0.0", + "proc-log": "^2.0.1", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/node-gyp/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/node-gyp/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/packager": { + "version": "18.4.4", + "resolved": "https://registry.npmjs.org/@electron/packager/-/packager-18.4.4.tgz", + "integrity": "sha512-fTUCmgL25WXTcFpM1M72VmFP8w3E4d+KNzWxmTDRpvwkfn/S206MAtM2cy0GF78KS9AwASMOUmlOIzCHeNxcGQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@electron/asar": "^3.2.13", + "@electron/get": "^3.0.0", + "@electron/notarize": "^2.1.0", + "@electron/osx-sign": "^1.0.5", + "@electron/universal": "^2.0.1", + "@electron/windows-sign": "^1.0.0", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.0.1", + "extract-zip": "^2.0.0", + "filenamify": "^4.1.0", + "fs-extra": "^11.1.0", + "galactus": "^1.0.0", + "get-package-info": "^1.0.0", + "junk": "^3.1.0", + "parse-author": "^2.0.0", + "plist": "^3.0.0", + "prettier": "^3.4.2", + "resedit": "^2.0.0", + "resolve": "^1.1.6", + "semver": "^7.1.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "electron-packager": "bin/electron-packager.js" + }, + "engines": { + "node": ">= 16.13.0" + }, + "funding": { + "url": "https://github.com/electron/packager?sponsor=1" + } + }, + "node_modules/@electron/packager/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.7.2.tgz", + "integrity": "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.2", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", + "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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/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/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@jest/reporters/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz", + "integrity": "sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.1.tgz", + "integrity": "sha512-HjhlEREguAyBTGNzRlGNiDHGQ2EjLSPWwdhhpoEqHYy8hWak3Dp6/fU72OfqVsiMb8S6rbfPsWUF24fxpilrVA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz", + "integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", + "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "postcss": "^8.4.41", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tybys/wasm-util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/@types/appdmg": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@types/appdmg/-/appdmg-0.5.5.tgz", + "integrity": "sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/electron-squirrel-startup": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/electron-squirrel-startup/-/electron-squirrel-startup-1.0.2.tgz", + "integrity": "sha512-AzxnvBzNh8K/0SmxMmZtpJf1/IWoGXLP+pQDuUaVkPyotI8ryvAtBSqgxR/qOSvxWHYWrxkeNsJ+Ca5xOuUxJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "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/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz", + "integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/papaparse": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.1.tgz", + "integrity": "sha512-esEO+VISsLIyE+JZBmb89NzsYYbpwV8lmv2rPo6oX5y9KhBaIP7hhHgjuTut54qjdKVMufTEcrh5fUl9+58huw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vscode/sudo-prompt": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz", + "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.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/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdmg": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/appdmg/-/appdmg-0.6.6.tgz", + "integrity": "sha512-GRmFKlCG+PWbcYF4LUNonTYmy0GjguDy6Jh9WP8mpd0T6j80XIJyXBiWlD0U+MLNhqV9Nhx49Gl9GpVToulpLg==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "async": "^1.4.2", + "ds-store": "^0.1.5", + "execa": "^1.0.0", + "fs-temp": "^1.0.0", + "fs-xattr": "^0.3.0", + "image-size": "^0.7.4", + "is-my-json-valid": "^2.20.0", + "minimist": "^1.1.3", + "parse-color": "^1.0.0", + "path-exists": "^4.0.0", + "repeat-string": "^1.5.4" + }, + "bin": { + "appdmg": "bin/appdmg.js" + }, + "engines": { + "node": ">=8.5" + } + }, + "node_modules/appdmg/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/appdmg/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/appdmg/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/appdmg/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/appdmg/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/appdmg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/appdmg/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/appdmg/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/appdmg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/author-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/author-regex/-/author-regex-1.0.0.tgz", + "integrity": "sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base32-encode": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-1.2.0.tgz", + "integrity": "sha512-cHFU8XeRyx0GgmoWi5qHMCVRiqU6J3MHWxVgun7jggCBUpVzm1Ir7M9dYr2whjSNc3tFeXfQ/oZjQu/4u55h9A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "to-data-view": "^1.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/better-sqlite3": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz", + "integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/bplist-creator": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.8.tgz", + "integrity": "sha512-Za9JKzD6fjLC16oX2wsXfc+qBEhJBJB1YPInoAQpMLhDuj5aVOv1baGeIQSq1Fr3OCqzvsoQcSBSwGId/Ja2PA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "stream-buffers": "~2.2.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT" + }, + "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/cross-zip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cross-zip/-/cross-zip-4.0.1.tgz", + "integrity": "sha512-n63i0lZ0rvQ6FXiGQ+/JFCKAUyPFhLQYJIqKaa+tSJtfKeULF/IDNDAbdnSIxgS4NTuw2b0+lj8LzfITuq+ZxQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.10" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ds-store": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/ds-store/-/ds-store-0.1.6.tgz", + "integrity": "sha512-kY21M6Lz+76OS3bnCzjdsJSF7LBpLYGCVfavW8TgQD2XkcqIZ86W0y9qUDZu6fp7SIZzqosMDW2zi7zVFfv4hw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bplist-creator": "~0.0.3", + "macos-alias": "~0.2.5", + "tn1150": "^0.1.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron": { + "version": "39.2.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.3.tgz", + "integrity": "sha512-j7k7/bj3cNA29ty54FzEMRUoqirE+RBQPhPFP+XDuM93a1l2WcDPiYumxKWz+iKcXxBJLFdMIAlvtLTB/RfCkg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-installer-common": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.4.tgz", + "integrity": "sha512-8gMNPXfAqUE5CfXg8RL0vXpLE9HAaPkgLXVoHE3BMUzogMWenf4LmwQ27BdCUrEhkjrKl+igs2IHJibclR3z3Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@electron/asar": "^3.2.5", + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "glob": "^7.1.4", + "lodash": "^4.17.15", + "parse-author": "^2.0.0", + "semver": "^7.1.1", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "url": "https://github.com/electron-userland/electron-installer-common?sponsor=1" + }, + "optionalDependencies": { + "@types/fs-extra": "^9.0.1" + } + }, + "node_modules/electron-installer-common/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-common/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/electron-installer-debian/-/electron-installer-debian-3.2.0.tgz", + "integrity": "sha512-58ZrlJ1HQY80VucsEIG9tQ//HrTlG6sfofA3nRGr6TmkX661uJyu4cMPPh6kXW+aHdq/7+q25KyQhDrXvRL7jw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.2", + "fs-extra": "^9.0.0", + "get-folder-size": "^2.0.1", + "lodash": "^4.17.4", + "word-wrap": "^1.2.3", + "yargs": "^16.0.2" + }, + "bin": { + "electron-installer-debian": "src/cli.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-installer-debian/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-debian/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/electron-installer-debian/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron-installer-debian/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-debian/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-debian/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-installer-debian/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-debian/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-dmg": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/electron-installer-dmg/-/electron-installer-dmg-5.0.1.tgz", + "integrity": "sha512-qOa1aAQdX57C+vzhDk3549dd/PRlNL4F8y736MTD1a43qptD+PvHY97Bo9gSf+OZ8iUWE7BrYSpk/FgLUe40EA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/appdmg": "^0.5.5", + "debug": "^4.3.2", + "minimist": "^1.2.7" + }, + "bin": { + "electron-installer-dmg": "dist/electron-installer-dmg-bin.js" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "appdmg": "^0.6.4" + } + }, + "node_modules/electron-installer-redhat": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/electron-installer-redhat/-/electron-installer-redhat-3.4.0.tgz", + "integrity": "sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "@malept/cross-spawn-promise": "^1.0.0", + "debug": "^4.1.1", + "electron-installer-common": "^0.10.2", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "word-wrap": "^1.2.3", + "yargs": "^16.0.2" + }, + "bin": { + "electron-installer-redhat": "src/cli.js" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-installer-redhat/node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/electron-installer-redhat/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/electron-installer-redhat/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/electron-installer-redhat/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-redhat/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-redhat/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-installer-redhat/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/electron-installer-redhat/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-installer-redhat/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-squirrel-startup": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/electron-squirrel-startup/-/electron-squirrel-startup-1.0.1.tgz", + "integrity": "sha512-sTfFIHGku+7PsHLJ7v0dRcZNkALrV+YEozINTW8X1nM//e5O3L+rfYuvSW00lmGHnYmUjARZulD8F2V8ISI9RA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.2.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/electron-squirrel-startup/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron-winstaller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-winstaller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/electron/node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/electron/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/electron/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/electron/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "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/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/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/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/esrecurse/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/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "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/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/flora-colossus": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/flora-colossus/-/flora-colossus-2.0.0.tgz", + "integrity": "sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fmix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fmix/-/fmix-0.1.0.tgz", + "integrity": "sha512-Y6hyofImk9JdzU8k5INtTXX1cu8LDlePWDFU5sftm9H+zKCr5SGrVjdhkvsim646cw5zD0nADj8oHyXMZmCZ9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "imul": "^1.0.0" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-temp": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-1.2.1.tgz", + "integrity": "sha512-okTwLB7/Qsq82G6iN5zZJFsOfZtx2/pqrA7Hk/9fvy+c+eJS9CvgGXT2uNxwnI14BDY9L/jQPkaBgSvlKfSW9w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "random-path": "^0.1.0" + } + }, + "node_modules/fs-xattr": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/fs-xattr/-/fs-xattr-0.3.1.tgz", + "integrity": "sha512-UVqkrEW0GfDabw4C3HOrFlxKfx0eeigfRne69FxSBdHIP8Qt5Sq6Pu3RM9KmMlkygtC4pPKkj5CiPO5USnj2GA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/galactus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/galactus/-/galactus-1.0.0.tgz", + "integrity": "sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "flora-colossus": "^2.0.0", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/gar": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/gar/-/gar-1.0.4.tgz", + "integrity": "sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha512-TuOwZWgJ2VAMEGJvAyPWvpqxSANF0LDpmyHauMjFYzaACvn+QTT/AZomvPCzVBV7yDN3OmwHQ5OvHaeLKre3JQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-property": "^1.0.0" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-folder-size": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-folder-size/-/get-folder-size-2.0.1.tgz", + "integrity": "sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "gar": "^1.0.4", + "tiny-each-async": "2.0.3" + }, + "bin": { + "get-folder-size": "bin/get-folder-size" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-info": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-package-info/-/get-package-info-1.0.0.tgz", + "integrity": "sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.1.1", + "debug": "^2.2.0", + "lodash.get": "^4.0.0", + "read-pkg-up": "^2.0.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/get-package-info/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/get-package-info/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/i18next": { + "version": "25.7.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz", + "integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/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/image-size": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imul": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", + "integrity": "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-my-ip-valid": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.1.tgz", + "integrity": "sha512-jxc8cBcOWbNK2i2aTkCZP6i7wkHF1bqKFrwEHuN5Jtg5BSaZHUZQ/JTOJwoV41YvHnOaRyWWh72T/KvfNz9DJg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-my-json-valid": { + "version": "2.20.6", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.20.6.tgz", + "integrity": "sha512-1JQwulVNjx8UqkPE/bqDaxtH4PXCe/2VRh/y3p99heOV87HG4Id5/VfDswd+YiAfHcRTfDlWgISycnHuhZq1aw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^5.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-config/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-config/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/jest-runtime/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jest-runtime/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "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/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/junk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", + "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-7.0.2.tgz", + "integrity": "sha512-rJysbR9GKIalhTbVL2tYbF2hVyDnrf7pFUZBwjPaMIdadYHmeT+EVi/Bu3qd7ETQPahTotg2WRCatXwRBW554g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^5.0.1", + "rfdc": "^1.3.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz", + "integrity": "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^5.0.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^5.0.0", + "strip-ansi": "^7.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", + "integrity": "sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.554.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz", + "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/macos-alias": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/macos-alias/-/macos-alias-0.2.12.tgz", + "integrity": "sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "nan": "^2.4.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mem": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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/murmur-32": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-0.2.0.tgz", + "integrity": "sha512-ZkcWZudylwF+ir3Ld1n7gL6bI2mQAzXvSobPwVtu8aYi2sbXeipeSkdcanRLzIofLcM5F53lGaKm2dk7orBi7Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "encode-utf8": "^1.0.3", + "fmix": "^0.1.0", + "imul": "^1.0.0" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nan": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.24.0.tgz", + "integrity": "sha512-Vpf9qnVW1RaDkoNKFUvfxqAbtI8ncb8OJlqZ9wwpXzWPEsvsB1nvdUi6oYrHIkQ1Y/tMDnr1h4nczS0VB9Xykg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-author": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-2.0.0.tgz", + "integrity": "sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "author-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-color": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz", + "integrity": "sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "~0.5.0" + } + }, + "node_modules/parse-color/node_modules/color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==", + "dev": true, + "optional": true + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-1.0.1.tgz", + "integrity": "sha512-nh39Mo1eGWmZS7y+mK/dQIqg7S1lp38DpRxkyoHf0ZcUs/HDc+yyTjuOtTvSMZHmfSLuSQaX945u05Y2Q6UWZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "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.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-2.0.1.tgz", + "integrity": "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.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/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/random-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/random-path/-/random-path-0.1.2.tgz", + "integrity": "sha512-4jY0yoEaQ5v9StCl5kZbNIQlg1QheIDBrdkDn53EynpPb9FgO6//p3X/tgMnrC45XN6QZCzU1Xz/+pSSsJBpRw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "base32-encode": "^0.1.0 || ^1.0.0", + "murmur-32": "^0.1.0 || ^0.2.0" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.1" + } + }, + "node_modules/react-i18next": { + "version": "16.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.4.1.tgz", + "integrity": "sha512-GzsYomxb1/uE7nlJm0e1qQ8f+W9I3Xirh9VoycZIahk6C8Pmv/9Fd0ek6zjf1FSgtGLElDGqwi/4FOHEGUbsEQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", + "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz", + "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==", + "license": "MIT", + "dependencies": { + "react-router": "7.10.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/recharts": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.1.tgz", + "integrity": "sha512-+v+HJojK7gnEgG6h+b2u7k8HH7FhyFUzAc4+cPrsjL4Otdgqr/ecXzAnHciqlzV1ko064eNcsdzrYOM78kankA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-2.0.3.tgz", + "integrity": "sha512-oTeemxwoMuxxTYxXUwjkrOPfngTQehlv0/HoYFNkB4uzsP1Un1A9nI8JQKGOFkxpqkC7qkMs0lUsGrvUlbLNUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^1.0.1" + }, + "engines": { + "node": ">=14", + "npm": ">=7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "dev": true, + "license": "Unlicense", + "optional": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", + "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.15.tgz", + "integrity": "sha512-PGkOdpRFK+rb1TzVz+msVhw4YMRT9txLF4kRqvJhGhCM324xuR3REBSHALN+l+sAhKUmz0aotnjp5D+P83mLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tiny-each-async": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tiny-each-async/-/tiny-each-async-2.0.3.tgz", + "integrity": "sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/tmp-promise/node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/tn1150": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tn1150/-/tn1150-0.1.0.tgz", + "integrity": "sha512-DbplOfQFkqG5IHcDyyrs/lkvSr3mPUVsFf/RbDppOshs22yTPnSJWEe6FkYd1txAwU/zcnR905ar2fi4kwF29w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "unorm": "^1.4.1" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/to-data-view": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-1.1.0.tgz", + "integrity": "sha512-1eAdufMg6mwgmlojAx3QeMnzB/BTVp7Tbndi3U7ftcT2zCZadjxkkmLmd97zmaxWi+sgGcgWrokmpEoy0Dn0vQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-repeated/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "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/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unorm": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", + "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==", + "dev": true, + "license": "MIT or GPL-2.0", + "optional": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.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/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/username": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", + "integrity": "sha512-PCKbdWw85JsYMvmCv5GH3kXmM66rCd9m1hBEDutPNv94b/pqCMT4NtcKyeWYvLFiE8b+ha1Jdl8XAaUdPn5QTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0", + "mem": "^4.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/username/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/username/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/username/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/username/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/username/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/username/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/username/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.103.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.103.0.tgz", + "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.26.3", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.4", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.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/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.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" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/.history/sample_data/hard_set_5/classrooms_20251221032802.csv b/.history/sample_data/hard_set_5/classrooms_20251221032802.csv new file mode 100644 index 0000000..ed70411 --- /dev/null +++ b/.history/sample_data/hard_set_5/classrooms_20251221032802.csv @@ -0,0 +1,11 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +R1;25 +R10;100 +R2;30 +R3;30 +R4;40 +R5;40 +R6;50 +R7;50 +R8;60 +R9;75 diff --git a/.history/sample_data/hard_set_5/classrooms_20251221044411.csv b/.history/sample_data/hard_set_5/classrooms_20251221044411.csv new file mode 100644 index 0000000..fc09e85 --- /dev/null +++ b/.history/sample_data/hard_set_5/classrooms_20251221044411.csv @@ -0,0 +1,13 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +R1;190 +R10;180 +R2;170 +R3;160 +R4;150 +R5;180 +R6;190 +R7;170 +R8;180 +R9;155 +R10;175 +R11;164 diff --git a/.history/sample_data/hard_set_5/courses_20251221031304.csv b/.history/sample_data/hard_set_5/courses_20251221031304.csv new file mode 100644 index 0000000..e5e6c88 --- /dev/null +++ b/.history/sample_data/hard_set_5/courses_20251221031304.csv @@ -0,0 +1,16 @@ +id,code,name +1,CSE101,Introduction to Computer Science +2,MAT202,Advanced Calculus +3,PHY301,Quantum Mechanics +4,EE401,Digital Signal Processing +5,HIS105,World History +6,ART202,Modern Art +7,MUS303,Music Theory +8,ECO210,Microeconomics +9,PSY401,Cognitive Psychology +10,SOC101,Introduction to Sociology +11,ENG101,English Composition +12,CHE301,Organic Chemistry +13,BIO202,Cell Biology +14,PHI401,Existentialism +15,GEO101,Physical Geography \ No newline at end of file diff --git a/.history/sample_data/hard_set_5/courses_20251221031416.csv b/.history/sample_data/hard_set_5/courses_20251221031416.csv new file mode 100644 index 0000000..0cbdcfa --- /dev/null +++ b/.history/sample_data/hard_set_5/courses_20251221031416.csv @@ -0,0 +1,16 @@ +ALL OF THE COURSES IN THE SYSTEM +1,CSE101,Introduction to Computer Science +2,MAT202,Advanced Calculus +3,PHY301,Quantum Mechanics +4,EE401,Digital Signal Processing +5,HIS105,World History +6,ART202,Modern Art +7,MUS303,Music Theory +8,ECO210,Microeconomics +9,PSY401,Cognitive Psychology +10,SOC101,Introduction to Sociology +11,ENG101,English Composition +12,CHE301,Organic Chemistry +13,BIO202,Cell Biology +14,PHI401,Existentialism +15,GEO101,Physical Geography \ No newline at end of file diff --git a/.history/sample_data/hard_set_5/students_20251221023928.csv b/.history/sample_data/hard_set_5/students_20251221023928.csv new file mode 100644 index 0000000..231c2bf --- /dev/null +++ b/.history/sample_data/hard_set_5/students_20251221023928.csv @@ -0,0 +1,501 @@ +id,name +S001,Student 001 +S002,Student 002 +S003,Student 003 +S004,Student 004 +S005,Student 005 +S006,Student 006 +S007,Student 007 +S008,Student 008 +S009,Student 009 +S010,Student 010 +S011,Student 011 +S012,Student 012 +S013,Student 013 +S014,Student 014 +S015,Student 015 +S016,Student 016 +S017,Student 017 +S018,Student 018 +S019,Student 019 +S020,Student 020 +S021,Student 021 +S022,Student 022 +S023,Student 023 +S024,Student 024 +S025,Student 025 +S026,Student 026 +S027,Student 027 +S028,Student 028 +S029,Student 029 +S030,Student 030 +S031,Student 031 +S032,Student 032 +S033,Student 033 +S034,Student 034 +S035,Student 035 +S036,Student 036 +S037,Student 037 +S038,Student 038 +S039,Student 039 +S040,Student 040 +S041,Student 041 +S042,Student 042 +S043,Student 043 +S044,Student 044 +S045,Student 045 +S046,Student 046 +S047,Student 047 +S048,Student 048 +S049,Student 049 +S050,Student 050 +S051,Student 051 +S052,Student 052 +S053,Student 053 +S054,Student 054 +S055,Student 055 +S056,Student 056 +S057,Student 057 +S058,Student 058 +S059,Student 059 +S060,Student 060 +S061,Student 061 +S062,Student 062 +S063,Student 063 +S064,Student 064 +S065,Student 065 +S066,Student 066 +S067,Student 067 +S068,Student 068 +S069,Student 069 +S070,Student 070 +S071,Student 071 +S072,Student 072 +S073,Student 073 +S074,Student 074 +S075,Student 075 +S076,Student 076 +S077,Student 077 +S078,Student 078 +S079,Student 079 +S080,Student 080 +S081,Student 081 +S082,Student 082 +S083,Student 083 +S084,Student 084 +S085,Student 085 +S086,Student 086 +S087,Student 087 +S088,Student 088 +S089,Student 089 +S090,Student 090 +S091,Student 091 +S092,Student 092 +S093,Student 093 +S094,Student 094 +S095,Student 095 +S096,Student 096 +S097,Student 097 +S098,Student 098 +S099,Student 099 +S100,Student 100 +S101,Student 101 +S102,Student 102 +S103,Student 103 +S104,Student 104 +S105,Student 105 +S106,Student 106 +S107,Student 107 +S108,Student 108 +S109,Student 109 +S110,Student 110 +S111,Student 111 +S112,Student 112 +S113,Student 113 +S114,Student 114 +S115,Student 115 +S116,Student 116 +S117,Student 117 +S118,Student 118 +S119,Student 119 +S120,Student 120 +S121,Student 121 +S122,Student 122 +S123,Student 123 +S124,Student 124 +S125,Student 125 +S126,Student 126 +S127,Student 127 +S128,Student 128 +S129,Student 129 +S130,Student 130 +S131,Student 131 +S132,Student 132 +S133,Student 133 +S134,Student 134 +S135,Student 135 +S136,Student 136 +S137,Student 137 +S138,Student 138 +S139,Student 139 +S140,Student 140 +S141,Student 141 +S142,Student 142 +S143,Student 143 +S144,Student 144 +S145,Student 145 +S146,Student 146 +S147,Student 147 +S148,Student 148 +S149,Student 149 +S150,Student 150 +S151,Student 151 +S152,Student 152 +S153,Student 153 +S154,Student 154 +S155,Student 155 +S156,Student 156 +S157,Student 157 +S158,Student 158 +S159,Student 159 +S160,Student 160 +S161,Student 161 +S162,Student 162 +S163,Student 163 +S164,Student 164 +S165,Student 165 +S166,Student 166 +S167,Student 167 +S168,Student 168 +S169,Student 169 +S170,Student 170 +S171,Student 171 +S172,Student 172 +S173,Student 173 +S174,Student 174 +S175,Student 175 +S176,Student 176 +S177,Student 177 +S178,Student 178 +S179,Student 179 +S180,Student 180 +S181,Student 181 +S182,Student 182 +S183,Student 183 +S184,Student 184 +S185,Student 185 +S186,Student 186 +S187,Student 187 +S188,Student 188 +S189,Student 189 +S190,Student 190 +S191,Student 191 +S192,Student 192 +S193,Student 193 +S194,Student 194 +S195,Student 195 +S196,Student 196 +S197,Student 197 +S198,Student 198 +S199,Student 199 +S200,Student 200 +S201,Student 201 +S202,Student 202 +S203,Student 203 +S204,Student 204 +S205,Student 205 +S206,Student 206 +S207,Student 207 +S208,Student 208 +S209,Student 209 +S210,Student 210 +S211,Student 211 +S212,Student 212 +S213,Student 213 +S214,Student 214 +S215,Student 215 +S216,Student 216 +S217,Student 217 +S218,Student 218 +S219,Student 219 +S220,Student 220 +S221,Student 221 +S222,Student 222 +S223,Student 223 +S224,Student 224 +S225,Student 225 +S226,Student 226 +S227,Student 227 +S228,Student 228 +S229,Student 229 +S230,Student 230 +S231,Student 231 +S232,Student 232 +S233,Student 233 +S234,Student 234 +S235,Student 235 +S236,Student 236 +S237,Student 237 +S238,Student 238 +S239,Student 239 +S240,Student 240 +S241,Student 241 +S242,Student 242 +S243,Student 243 +S244,Student 244 +S245,Student 245 +S246,Student 246 +S247,Student 247 +S248,Student 248 +S249,Student 249 +S250,Student 250 +S251,Student 251 +S252,Student 252 +S253,Student 253 +S254,Student 254 +S255,Student 255 +S256,Student 256 +S257,Student 257 +S258,Student 258 +S259,Student 259 +S260,Student 260 +S261,Student 261 +S262,Student 262 +S263,Student 263 +S264,Student 264 +S265,Student 265 +S266,Student 266 +S267,Student 267 +S268,Student 268 +S269,Student 269 +S270,Student 270 +S271,Student 271 +S272,Student 272 +S273,Student 273 +S274,Student 274 +S275,Student 275 +S276,Student 276 +S277,Student 277 +S278,Student 278 +S279,Student 279 +S280,Student 280 +S281,Student 281 +S282,Student 282 +S283,Student 283 +S284,Student 284 +S285,Student 285 +S286,Student 286 +S287,Student 287 +S288,Student 288 +S289,Student 289 +S290,Student 290 +S291,Student 291 +S292,Student 292 +S293,Student 293 +S294,Student 294 +S295,Student 295 +S296,Student 296 +S297,Student 297 +S298,Student 298 +S299,Student 299 +S300,Student 300 +S301,Student 301 +S302,Student 302 +S303,Student 303 +S304,Student 304 +S305,Student 305 +S306,Student 306 +S307,Student 307 +S308,Student 308 +S309,Student 309 +S310,Student 310 +S311,Student 311 +S312,Student 312 +S313,Student 313 +S314,Student 314 +S315,Student 315 +S316,Student 316 +S317,Student 317 +S318,Student 318 +S319,Student 319 +S320,Student 320 +S321,Student 221 +S322,Student 322 +S323,Student 323 +S324,Student 324 +S325,Student 325 +S326,Student 326 +S327,Student 327 +S328,Student 328 +S329,Student 329 +S330,Student 330 +S331,Student 331 +S332,Student 332 +S333,Student 333 +S334,Student 334 +S335,Student 335 +S336,Student 336 +S337,Student 337 +S338,Student 338 +S339,Student 339 +S340,Student 340 +S341,Student 341 +S342,Student 342 +S343,Student 343 +S344,Student 344 +S345,Student 345 +S346,Student 346 +S347,Student 347 +S348,Student 348 +S349,Student 349 +S350,Student 350 +S351,Student 351 +S352,Student 352 +S353,Student 353 +S354,Student 354 +S355,Student 355 +S356,Student 356 +S357,Student 357 +S358,Student 358 +S359,Student 359 +S360,Student 360 +S361,Student 361 +S362,Student 362 +S363,Student 363 +S364,Student 364 +S365,Student 365 +S366,Student 366 +S367,Student 367 +S368,Student 368 +S369,Student 369 +S370,Student 370 +S371,Student 371 +S372,Student 372 +S373,Student 373 +S374,Student 374 +S375,Student 375 +S376,Student 376 +S377,Student 377 +S378,Student 378 +S379,Student 379 +S380,Student 380 +S381,Student 381 +S382,Student 382 +S383,Student 383 +S384,Student 384 +S385,Student 385 +S386,Student 386 +S387,Student 387 +S388,Student 388 +S389,Student 389 +S390,Student 390 +S391,Student 391 +S392,Student 392 +S393,Student 393 +S394,Student 394 +S395,Student 395 +S396,Student 396 +S397,Student 397 +S398,Student 398 +S399,Student 399 +S400,Student 400 +S401,Student 401 +S402,Student 402 +S403,Student 403 +S404,Student 404 +S405,Student 405 +S406,Student 406 +S407,Student 407 +S408,Student 408 +S409,Student 409 +S410,Student 410 +S411,Student 411 +S412,Student 412 +S413,Student 413 +S414,Student 414 +S415,Student 415 +S416,Student 416 +S417,Student 417 +S418,Student 418 +S419,Student 419 +S420,Student 420 +S421,Student 421 +S422,Student 422 +S423,Student 423 +S424,Student 424 +S425,Student 425 +S426,Student 426 +S427,Student 427 +S428,Student 428 +S429,Student 429 +S430,Student 430 +S431,Student 431 +S432,Student 432 +S433,Student 433 +S434,Student 434 +S435,Student 435 +S436,Student 436 +S437,Student 437 +S438,Student 438 +S439,Student 439 +S440,Student 440 +S441,Student 441 +S442,Student 442 +S443,Student 443 +S444,Student 444 +S445,Student 445 +S446,Student 446 +S447,Student 447 +S448,Student 448 +S449,Student 449 +S450,Student 450 +S451,Student 451 +S452,Student 452 +S453,Student 453 +S454,Student 454 +S455,Student 455 +S456,Student 456 +S457,Student 457 +S458,Student 458 +S459,Student 459 +S460,Student 460 +S461,Student 461 +S462,Student 462 +S463,Student 463 +S464,Student 464 +S465,Student 465 +S466,Student 466 +S467,Student 467 +S468,Student 468 +S469,Student 469 +S470,Student 470 +S471,Student 471 +S472,Student 472 +S473,Student 473 +S474,Student 474 +S475,Student 475 +S476,Student 476 +S477,Student 477 +S478,Student 478 +S479,Student 479 +S480,Student 480 +S481,Student 481 +S482,Student 482 +S483,Student 483 +S484,Student 484 +S485,Student 485 +S486,Student 486 +S487,Student 487 +S488,Student 488 +S489,Student 489 +S490,Student 490 +S491,Student 491 +S492,Student 492 +S493,Student 493 +S494,Student 494 +S495,Student 495 +S496,Student 496 +S497,Student 497 +S498,Student 498 +S499,Student 499 +S500,Student 500 diff --git a/.history/sample_data/hard_set_5/students_20251221031452.csv b/.history/sample_data/hard_set_5/students_20251221031452.csv new file mode 100644 index 0000000..00a5028 --- /dev/null +++ b/.history/sample_data/hard_set_5/students_20251221031452.csv @@ -0,0 +1,501 @@ + +S001,Student 001 +S002,Student 002 +S003,Student 003 +S004,Student 004 +S005,Student 005 +S006,Student 006 +S007,Student 007 +S008,Student 008 +S009,Student 009 +S010,Student 010 +S011,Student 011 +S012,Student 012 +S013,Student 013 +S014,Student 014 +S015,Student 015 +S016,Student 016 +S017,Student 017 +S018,Student 018 +S019,Student 019 +S020,Student 020 +S021,Student 021 +S022,Student 022 +S023,Student 023 +S024,Student 024 +S025,Student 025 +S026,Student 026 +S027,Student 027 +S028,Student 028 +S029,Student 029 +S030,Student 030 +S031,Student 031 +S032,Student 032 +S033,Student 033 +S034,Student 034 +S035,Student 035 +S036,Student 036 +S037,Student 037 +S038,Student 038 +S039,Student 039 +S040,Student 040 +S041,Student 041 +S042,Student 042 +S043,Student 043 +S044,Student 044 +S045,Student 045 +S046,Student 046 +S047,Student 047 +S048,Student 048 +S049,Student 049 +S050,Student 050 +S051,Student 051 +S052,Student 052 +S053,Student 053 +S054,Student 054 +S055,Student 055 +S056,Student 056 +S057,Student 057 +S058,Student 058 +S059,Student 059 +S060,Student 060 +S061,Student 061 +S062,Student 062 +S063,Student 063 +S064,Student 064 +S065,Student 065 +S066,Student 066 +S067,Student 067 +S068,Student 068 +S069,Student 069 +S070,Student 070 +S071,Student 071 +S072,Student 072 +S073,Student 073 +S074,Student 074 +S075,Student 075 +S076,Student 076 +S077,Student 077 +S078,Student 078 +S079,Student 079 +S080,Student 080 +S081,Student 081 +S082,Student 082 +S083,Student 083 +S084,Student 084 +S085,Student 085 +S086,Student 086 +S087,Student 087 +S088,Student 088 +S089,Student 089 +S090,Student 090 +S091,Student 091 +S092,Student 092 +S093,Student 093 +S094,Student 094 +S095,Student 095 +S096,Student 096 +S097,Student 097 +S098,Student 098 +S099,Student 099 +S100,Student 100 +S101,Student 101 +S102,Student 102 +S103,Student 103 +S104,Student 104 +S105,Student 105 +S106,Student 106 +S107,Student 107 +S108,Student 108 +S109,Student 109 +S110,Student 110 +S111,Student 111 +S112,Student 112 +S113,Student 113 +S114,Student 114 +S115,Student 115 +S116,Student 116 +S117,Student 117 +S118,Student 118 +S119,Student 119 +S120,Student 120 +S121,Student 121 +S122,Student 122 +S123,Student 123 +S124,Student 124 +S125,Student 125 +S126,Student 126 +S127,Student 127 +S128,Student 128 +S129,Student 129 +S130,Student 130 +S131,Student 131 +S132,Student 132 +S133,Student 133 +S134,Student 134 +S135,Student 135 +S136,Student 136 +S137,Student 137 +S138,Student 138 +S139,Student 139 +S140,Student 140 +S141,Student 141 +S142,Student 142 +S143,Student 143 +S144,Student 144 +S145,Student 145 +S146,Student 146 +S147,Student 147 +S148,Student 148 +S149,Student 149 +S150,Student 150 +S151,Student 151 +S152,Student 152 +S153,Student 153 +S154,Student 154 +S155,Student 155 +S156,Student 156 +S157,Student 157 +S158,Student 158 +S159,Student 159 +S160,Student 160 +S161,Student 161 +S162,Student 162 +S163,Student 163 +S164,Student 164 +S165,Student 165 +S166,Student 166 +S167,Student 167 +S168,Student 168 +S169,Student 169 +S170,Student 170 +S171,Student 171 +S172,Student 172 +S173,Student 173 +S174,Student 174 +S175,Student 175 +S176,Student 176 +S177,Student 177 +S178,Student 178 +S179,Student 179 +S180,Student 180 +S181,Student 181 +S182,Student 182 +S183,Student 183 +S184,Student 184 +S185,Student 185 +S186,Student 186 +S187,Student 187 +S188,Student 188 +S189,Student 189 +S190,Student 190 +S191,Student 191 +S192,Student 192 +S193,Student 193 +S194,Student 194 +S195,Student 195 +S196,Student 196 +S197,Student 197 +S198,Student 198 +S199,Student 199 +S200,Student 200 +S201,Student 201 +S202,Student 202 +S203,Student 203 +S204,Student 204 +S205,Student 205 +S206,Student 206 +S207,Student 207 +S208,Student 208 +S209,Student 209 +S210,Student 210 +S211,Student 211 +S212,Student 212 +S213,Student 213 +S214,Student 214 +S215,Student 215 +S216,Student 216 +S217,Student 217 +S218,Student 218 +S219,Student 219 +S220,Student 220 +S221,Student 221 +S222,Student 222 +S223,Student 223 +S224,Student 224 +S225,Student 225 +S226,Student 226 +S227,Student 227 +S228,Student 228 +S229,Student 229 +S230,Student 230 +S231,Student 231 +S232,Student 232 +S233,Student 233 +S234,Student 234 +S235,Student 235 +S236,Student 236 +S237,Student 237 +S238,Student 238 +S239,Student 239 +S240,Student 240 +S241,Student 241 +S242,Student 242 +S243,Student 243 +S244,Student 244 +S245,Student 245 +S246,Student 246 +S247,Student 247 +S248,Student 248 +S249,Student 249 +S250,Student 250 +S251,Student 251 +S252,Student 252 +S253,Student 253 +S254,Student 254 +S255,Student 255 +S256,Student 256 +S257,Student 257 +S258,Student 258 +S259,Student 259 +S260,Student 260 +S261,Student 261 +S262,Student 262 +S263,Student 263 +S264,Student 264 +S265,Student 265 +S266,Student 266 +S267,Student 267 +S268,Student 268 +S269,Student 269 +S270,Student 270 +S271,Student 271 +S272,Student 272 +S273,Student 273 +S274,Student 274 +S275,Student 275 +S276,Student 276 +S277,Student 277 +S278,Student 278 +S279,Student 279 +S280,Student 280 +S281,Student 281 +S282,Student 282 +S283,Student 283 +S284,Student 284 +S285,Student 285 +S286,Student 286 +S287,Student 287 +S288,Student 288 +S289,Student 289 +S290,Student 290 +S291,Student 291 +S292,Student 292 +S293,Student 293 +S294,Student 294 +S295,Student 295 +S296,Student 296 +S297,Student 297 +S298,Student 298 +S299,Student 299 +S300,Student 300 +S301,Student 301 +S302,Student 302 +S303,Student 303 +S304,Student 304 +S305,Student 305 +S306,Student 306 +S307,Student 307 +S308,Student 308 +S309,Student 309 +S310,Student 310 +S311,Student 311 +S312,Student 312 +S313,Student 313 +S314,Student 314 +S315,Student 315 +S316,Student 316 +S317,Student 317 +S318,Student 318 +S319,Student 319 +S320,Student 320 +S321,Student 221 +S322,Student 322 +S323,Student 323 +S324,Student 324 +S325,Student 325 +S326,Student 326 +S327,Student 327 +S328,Student 328 +S329,Student 329 +S330,Student 330 +S331,Student 331 +S332,Student 332 +S333,Student 333 +S334,Student 334 +S335,Student 335 +S336,Student 336 +S337,Student 337 +S338,Student 338 +S339,Student 339 +S340,Student 340 +S341,Student 341 +S342,Student 342 +S343,Student 343 +S344,Student 344 +S345,Student 345 +S346,Student 346 +S347,Student 347 +S348,Student 348 +S349,Student 349 +S350,Student 350 +S351,Student 351 +S352,Student 352 +S353,Student 353 +S354,Student 354 +S355,Student 355 +S356,Student 356 +S357,Student 357 +S358,Student 358 +S359,Student 359 +S360,Student 360 +S361,Student 361 +S362,Student 362 +S363,Student 363 +S364,Student 364 +S365,Student 365 +S366,Student 366 +S367,Student 367 +S368,Student 368 +S369,Student 369 +S370,Student 370 +S371,Student 371 +S372,Student 372 +S373,Student 373 +S374,Student 374 +S375,Student 375 +S376,Student 376 +S377,Student 377 +S378,Student 378 +S379,Student 379 +S380,Student 380 +S381,Student 381 +S382,Student 382 +S383,Student 383 +S384,Student 384 +S385,Student 385 +S386,Student 386 +S387,Student 387 +S388,Student 388 +S389,Student 389 +S390,Student 390 +S391,Student 391 +S392,Student 392 +S393,Student 393 +S394,Student 394 +S395,Student 395 +S396,Student 396 +S397,Student 397 +S398,Student 398 +S399,Student 399 +S400,Student 400 +S401,Student 401 +S402,Student 402 +S403,Student 403 +S404,Student 404 +S405,Student 405 +S406,Student 406 +S407,Student 407 +S408,Student 408 +S409,Student 409 +S410,Student 410 +S411,Student 411 +S412,Student 412 +S413,Student 413 +S414,Student 414 +S415,Student 415 +S416,Student 416 +S417,Student 417 +S418,Student 418 +S419,Student 419 +S420,Student 420 +S421,Student 421 +S422,Student 422 +S423,Student 423 +S424,Student 424 +S425,Student 425 +S426,Student 426 +S427,Student 427 +S428,Student 428 +S429,Student 429 +S430,Student 430 +S431,Student 431 +S432,Student 432 +S433,Student 433 +S434,Student 434 +S435,Student 435 +S436,Student 436 +S437,Student 437 +S438,Student 438 +S439,Student 439 +S440,Student 440 +S441,Student 441 +S442,Student 442 +S443,Student 443 +S444,Student 444 +S445,Student 445 +S446,Student 446 +S447,Student 447 +S448,Student 448 +S449,Student 449 +S450,Student 450 +S451,Student 451 +S452,Student 452 +S453,Student 453 +S454,Student 454 +S455,Student 455 +S456,Student 456 +S457,Student 457 +S458,Student 458 +S459,Student 459 +S460,Student 460 +S461,Student 461 +S462,Student 462 +S463,Student 463 +S464,Student 464 +S465,Student 465 +S466,Student 466 +S467,Student 467 +S468,Student 468 +S469,Student 469 +S470,Student 470 +S471,Student 471 +S472,Student 472 +S473,Student 473 +S474,Student 474 +S475,Student 475 +S476,Student 476 +S477,Student 477 +S478,Student 478 +S479,Student 479 +S480,Student 480 +S481,Student 481 +S482,Student 482 +S483,Student 483 +S484,Student 484 +S485,Student 485 +S486,Student 486 +S487,Student 487 +S488,Student 488 +S489,Student 489 +S490,Student 490 +S491,Student 491 +S492,Student 492 +S493,Student 493 +S494,Student 494 +S495,Student 495 +S496,Student 496 +S497,Student 497 +S498,Student 498 +S499,Student 499 +S500,Student 500 diff --git a/.history/src/app_20251221022001.tsx b/.history/src/app_20251221022001.tsx new file mode 100644 index 0000000..9416cc0 --- /dev/null +++ b/.history/src/app_20251221022001.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Sidebar } from './components/Sidebar'; +import { Dashboard } from './components/Dashboard'; +import { DataInput } from './components/DataInput'; +import { ScheduleView } from './components/ScheduleView'; +import { Settings } from './components/Settings'; +import { ConstraintSelector } from './components/ConstraintSelector'; +import { HelpModal } from './components/HelpModal'; +import { ViewMode, Course, Classroom, Student, ExamSession, GenerationConstraints } from './types'; +import { NotificationProvider } from './context/NotificationContext'; + +const App: React.FC = () => { + const { t } = useTranslation(); + const [currentView, setCurrentView] = useState(ViewMode.DASHBOARD); + + const [courses, setCourses] = useState([]); + const [classrooms, setClassrooms] = useState([]); + const [students, setStudents] = useState([]); + const [schedule, setSchedule] = useState([]); + const [isGenerated, setIsGenerated] = useState(false); + const [showConstraintModal, setShowConstraintModal] = useState(false); + const [showHelpModal, setShowHelpModal] = useState(false); + + const [isGenerating, setIsGenerating] = useState(false); + const [generationError, setGenerationError] = useState(null); + + // Function to load data from the backend + const loadData = async () => { + try { + const savedCourses = await window.api.getCourses(); + const savedClassrooms = await window.api.getClassrooms(); + const savedStudents = await window.api.getStudents(); + + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + setClassrooms(savedClassrooms.map((r: any) => ({ + id: r.id.toString(), + name: r.name, + capacity: r.capacity, + building: r.building + }))); + + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + // Load saved schedule from database + const savedScheduleResult = await window.api.loadSchedule(); + if (savedScheduleResult.success && savedScheduleResult.sessions.length > 0) { + const loadedSchedule = savedScheduleResult.sessions.map((s: any) => ({ + id: s.session_id, + courseId: s.course_code, + classroomId: s.classroom_name, + startTime: new Date(s.start_time), + endTime: new Date(s.end_time), + studentCount: s.student_count, + })); + setSchedule(loadedSchedule); + setIsGenerated(true); + console.log(`Loaded ${loadedSchedule.length} saved exam sessions from database.`); + } + } catch (error) { + console.error("Failed to load data:", error); + } + }; + + // Initial data load on mount + useEffect(() => { + loadData(); + + const handleNavigate = (event: CustomEvent) => { + const viewName = event.detail; + const viewMap: { [key: string]: ViewMode } = { + 'Dashboard': ViewMode.DASHBOARD, + 'Data Management': ViewMode.DATA, + 'Schedule View': ViewMode.SCHEDULE, + 'Settings': ViewMode.SETTINGS, + }; + if (viewMap[viewName]) { + setCurrentView(viewMap[viewName]); + } + }; + + window.addEventListener('navigate', handleNavigate as EventListener); + return () => { + window.removeEventListener('navigate', handleNavigate as EventListener); + }; + }, []); + + // Auto-refresh data when switching to Data Management tab + useEffect(() => { + if (currentView === ViewMode.DATA) { + loadData(); + } + }, [currentView]); + + const handleGenerateSchedule = () => { + setShowConstraintModal(true); + setGenerationError(null); + }; + + const handleFinalizeSchedule = async (constraints: GenerationConstraints) => { + setShowConstraintModal(false); + setIsGenerating(true); + setGenerationError(null); + + // Helper to format date as local ISO string (no UTC conversion) + const toLocalISOString = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; + }; + + try { + // Call the real scheduling algorithm via IPC (use local dates, not UTC) + const result = await window.api.generateSchedule({ + startDate: toLocalISOString(constraints.startDate), + endDate: toLocalISOString(constraints.endDate), + includeWeekends: constraints.includeWeekends, + dailyStartTime: constraints.dailyStartTime, + dailyEndTime: constraints.dailyEndTime, + maxExamsPerDay: constraints.maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams, + minHoursBetweenExams: constraints.minHoursBetweenExams, + }); + + if (result.success) { + // Convert date strings back to Date objects + const scheduleWithDates = result.schedule.map((session: any) => ({ + ...session, + startTime: new Date(session.startTime), + endTime: new Date(session.endTime), + })); + + setSchedule(scheduleWithDates); + setIsGenerated(true); + setCurrentView(ViewMode.SCHEDULE); + + // Auto-save the generated schedule to database (keep as local strings) + const sessionsForSave = result.schedule.map((s: any) => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime, + endTime: s.endTime, + studentCount: s.studentCount, + })); + await window.api.saveSchedule(sessionsForSave); + + console.log(`Schedule generated and saved successfully in ${result.stats?.generationTimeMs}ms`); + } else { + // No valid schedule found - show user-friendly error + const errorWithSuggestion = `${result.message}\n\nSuggestions:\n• Add more classrooms\n• Extend the exam date range\n• Reduce overlapping student enrollments`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation failed:', result.message); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; + const errorWithSuggestion = `${errorMessage}\n\nPlease check your data and try again.`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation error:', error); + } finally { + setIsGenerating(false); + } + }; + + const renderContent = () => { + switch (currentView) { + case ViewMode.DASHBOARD: + return ( + + ); + case ViewMode.DATA: + return ( + + ); + case ViewMode.SCHEDULE: + return ( + + ); + case ViewMode.SETTINGS: + return ; + default: + return
View not found
; + } + }; + + return ( + +
+ +
+
+

+ {currentView === ViewMode.DASHBOARD && t('common.dashboard')} + {currentView === ViewMode.DATA && t('common.dataManagement')} + {currentView === ViewMode.SCHEDULE && t('common.scheduleView')} + {currentView === ViewMode.SETTINGS && t('common.settings')} +

+ +
+
+ {renderContent()} +
+ + {showConstraintModal && ( + setShowConstraintModal(false)} + onGenerate={handleFinalizeSchedule} + /> + )} + + setShowHelpModal(false)} + currentView={currentView} + /> +
+
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/.history/src/app_20251221041613.tsx b/.history/src/app_20251221041613.tsx new file mode 100644 index 0000000..9416cc0 --- /dev/null +++ b/.history/src/app_20251221041613.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Sidebar } from './components/Sidebar'; +import { Dashboard } from './components/Dashboard'; +import { DataInput } from './components/DataInput'; +import { ScheduleView } from './components/ScheduleView'; +import { Settings } from './components/Settings'; +import { ConstraintSelector } from './components/ConstraintSelector'; +import { HelpModal } from './components/HelpModal'; +import { ViewMode, Course, Classroom, Student, ExamSession, GenerationConstraints } from './types'; +import { NotificationProvider } from './context/NotificationContext'; + +const App: React.FC = () => { + const { t } = useTranslation(); + const [currentView, setCurrentView] = useState(ViewMode.DASHBOARD); + + const [courses, setCourses] = useState([]); + const [classrooms, setClassrooms] = useState([]); + const [students, setStudents] = useState([]); + const [schedule, setSchedule] = useState([]); + const [isGenerated, setIsGenerated] = useState(false); + const [showConstraintModal, setShowConstraintModal] = useState(false); + const [showHelpModal, setShowHelpModal] = useState(false); + + const [isGenerating, setIsGenerating] = useState(false); + const [generationError, setGenerationError] = useState(null); + + // Function to load data from the backend + const loadData = async () => { + try { + const savedCourses = await window.api.getCourses(); + const savedClassrooms = await window.api.getClassrooms(); + const savedStudents = await window.api.getStudents(); + + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + setClassrooms(savedClassrooms.map((r: any) => ({ + id: r.id.toString(), + name: r.name, + capacity: r.capacity, + building: r.building + }))); + + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + // Load saved schedule from database + const savedScheduleResult = await window.api.loadSchedule(); + if (savedScheduleResult.success && savedScheduleResult.sessions.length > 0) { + const loadedSchedule = savedScheduleResult.sessions.map((s: any) => ({ + id: s.session_id, + courseId: s.course_code, + classroomId: s.classroom_name, + startTime: new Date(s.start_time), + endTime: new Date(s.end_time), + studentCount: s.student_count, + })); + setSchedule(loadedSchedule); + setIsGenerated(true); + console.log(`Loaded ${loadedSchedule.length} saved exam sessions from database.`); + } + } catch (error) { + console.error("Failed to load data:", error); + } + }; + + // Initial data load on mount + useEffect(() => { + loadData(); + + const handleNavigate = (event: CustomEvent) => { + const viewName = event.detail; + const viewMap: { [key: string]: ViewMode } = { + 'Dashboard': ViewMode.DASHBOARD, + 'Data Management': ViewMode.DATA, + 'Schedule View': ViewMode.SCHEDULE, + 'Settings': ViewMode.SETTINGS, + }; + if (viewMap[viewName]) { + setCurrentView(viewMap[viewName]); + } + }; + + window.addEventListener('navigate', handleNavigate as EventListener); + return () => { + window.removeEventListener('navigate', handleNavigate as EventListener); + }; + }, []); + + // Auto-refresh data when switching to Data Management tab + useEffect(() => { + if (currentView === ViewMode.DATA) { + loadData(); + } + }, [currentView]); + + const handleGenerateSchedule = () => { + setShowConstraintModal(true); + setGenerationError(null); + }; + + const handleFinalizeSchedule = async (constraints: GenerationConstraints) => { + setShowConstraintModal(false); + setIsGenerating(true); + setGenerationError(null); + + // Helper to format date as local ISO string (no UTC conversion) + const toLocalISOString = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; + }; + + try { + // Call the real scheduling algorithm via IPC (use local dates, not UTC) + const result = await window.api.generateSchedule({ + startDate: toLocalISOString(constraints.startDate), + endDate: toLocalISOString(constraints.endDate), + includeWeekends: constraints.includeWeekends, + dailyStartTime: constraints.dailyStartTime, + dailyEndTime: constraints.dailyEndTime, + maxExamsPerDay: constraints.maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams, + minHoursBetweenExams: constraints.minHoursBetweenExams, + }); + + if (result.success) { + // Convert date strings back to Date objects + const scheduleWithDates = result.schedule.map((session: any) => ({ + ...session, + startTime: new Date(session.startTime), + endTime: new Date(session.endTime), + })); + + setSchedule(scheduleWithDates); + setIsGenerated(true); + setCurrentView(ViewMode.SCHEDULE); + + // Auto-save the generated schedule to database (keep as local strings) + const sessionsForSave = result.schedule.map((s: any) => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime, + endTime: s.endTime, + studentCount: s.studentCount, + })); + await window.api.saveSchedule(sessionsForSave); + + console.log(`Schedule generated and saved successfully in ${result.stats?.generationTimeMs}ms`); + } else { + // No valid schedule found - show user-friendly error + const errorWithSuggestion = `${result.message}\n\nSuggestions:\n• Add more classrooms\n• Extend the exam date range\n• Reduce overlapping student enrollments`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation failed:', result.message); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; + const errorWithSuggestion = `${errorMessage}\n\nPlease check your data and try again.`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation error:', error); + } finally { + setIsGenerating(false); + } + }; + + const renderContent = () => { + switch (currentView) { + case ViewMode.DASHBOARD: + return ( + + ); + case ViewMode.DATA: + return ( + + ); + case ViewMode.SCHEDULE: + return ( + + ); + case ViewMode.SETTINGS: + return ; + default: + return
View not found
; + } + }; + + return ( + +
+ +
+
+

+ {currentView === ViewMode.DASHBOARD && t('common.dashboard')} + {currentView === ViewMode.DATA && t('common.dataManagement')} + {currentView === ViewMode.SCHEDULE && t('common.scheduleView')} + {currentView === ViewMode.SETTINGS && t('common.settings')} +

+ +
+
+ {renderContent()} +
+ + {showConstraintModal && ( + setShowConstraintModal(false)} + onGenerate={handleFinalizeSchedule} + /> + )} + + setShowHelpModal(false)} + currentView={currentView} + /> +
+
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/.history/src/app_20251221041614.tsx b/.history/src/app_20251221041614.tsx new file mode 100644 index 0000000..9416cc0 --- /dev/null +++ b/.history/src/app_20251221041614.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Sidebar } from './components/Sidebar'; +import { Dashboard } from './components/Dashboard'; +import { DataInput } from './components/DataInput'; +import { ScheduleView } from './components/ScheduleView'; +import { Settings } from './components/Settings'; +import { ConstraintSelector } from './components/ConstraintSelector'; +import { HelpModal } from './components/HelpModal'; +import { ViewMode, Course, Classroom, Student, ExamSession, GenerationConstraints } from './types'; +import { NotificationProvider } from './context/NotificationContext'; + +const App: React.FC = () => { + const { t } = useTranslation(); + const [currentView, setCurrentView] = useState(ViewMode.DASHBOARD); + + const [courses, setCourses] = useState([]); + const [classrooms, setClassrooms] = useState([]); + const [students, setStudents] = useState([]); + const [schedule, setSchedule] = useState([]); + const [isGenerated, setIsGenerated] = useState(false); + const [showConstraintModal, setShowConstraintModal] = useState(false); + const [showHelpModal, setShowHelpModal] = useState(false); + + const [isGenerating, setIsGenerating] = useState(false); + const [generationError, setGenerationError] = useState(null); + + // Function to load data from the backend + const loadData = async () => { + try { + const savedCourses = await window.api.getCourses(); + const savedClassrooms = await window.api.getClassrooms(); + const savedStudents = await window.api.getStudents(); + + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + setClassrooms(savedClassrooms.map((r: any) => ({ + id: r.id.toString(), + name: r.name, + capacity: r.capacity, + building: r.building + }))); + + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + // Load saved schedule from database + const savedScheduleResult = await window.api.loadSchedule(); + if (savedScheduleResult.success && savedScheduleResult.sessions.length > 0) { + const loadedSchedule = savedScheduleResult.sessions.map((s: any) => ({ + id: s.session_id, + courseId: s.course_code, + classroomId: s.classroom_name, + startTime: new Date(s.start_time), + endTime: new Date(s.end_time), + studentCount: s.student_count, + })); + setSchedule(loadedSchedule); + setIsGenerated(true); + console.log(`Loaded ${loadedSchedule.length} saved exam sessions from database.`); + } + } catch (error) { + console.error("Failed to load data:", error); + } + }; + + // Initial data load on mount + useEffect(() => { + loadData(); + + const handleNavigate = (event: CustomEvent) => { + const viewName = event.detail; + const viewMap: { [key: string]: ViewMode } = { + 'Dashboard': ViewMode.DASHBOARD, + 'Data Management': ViewMode.DATA, + 'Schedule View': ViewMode.SCHEDULE, + 'Settings': ViewMode.SETTINGS, + }; + if (viewMap[viewName]) { + setCurrentView(viewMap[viewName]); + } + }; + + window.addEventListener('navigate', handleNavigate as EventListener); + return () => { + window.removeEventListener('navigate', handleNavigate as EventListener); + }; + }, []); + + // Auto-refresh data when switching to Data Management tab + useEffect(() => { + if (currentView === ViewMode.DATA) { + loadData(); + } + }, [currentView]); + + const handleGenerateSchedule = () => { + setShowConstraintModal(true); + setGenerationError(null); + }; + + const handleFinalizeSchedule = async (constraints: GenerationConstraints) => { + setShowConstraintModal(false); + setIsGenerating(true); + setGenerationError(null); + + // Helper to format date as local ISO string (no UTC conversion) + const toLocalISOString = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; + }; + + try { + // Call the real scheduling algorithm via IPC (use local dates, not UTC) + const result = await window.api.generateSchedule({ + startDate: toLocalISOString(constraints.startDate), + endDate: toLocalISOString(constraints.endDate), + includeWeekends: constraints.includeWeekends, + dailyStartTime: constraints.dailyStartTime, + dailyEndTime: constraints.dailyEndTime, + maxExamsPerDay: constraints.maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams, + minHoursBetweenExams: constraints.minHoursBetweenExams, + }); + + if (result.success) { + // Convert date strings back to Date objects + const scheduleWithDates = result.schedule.map((session: any) => ({ + ...session, + startTime: new Date(session.startTime), + endTime: new Date(session.endTime), + })); + + setSchedule(scheduleWithDates); + setIsGenerated(true); + setCurrentView(ViewMode.SCHEDULE); + + // Auto-save the generated schedule to database (keep as local strings) + const sessionsForSave = result.schedule.map((s: any) => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime, + endTime: s.endTime, + studentCount: s.studentCount, + })); + await window.api.saveSchedule(sessionsForSave); + + console.log(`Schedule generated and saved successfully in ${result.stats?.generationTimeMs}ms`); + } else { + // No valid schedule found - show user-friendly error + const errorWithSuggestion = `${result.message}\n\nSuggestions:\n• Add more classrooms\n• Extend the exam date range\n• Reduce overlapping student enrollments`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation failed:', result.message); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; + const errorWithSuggestion = `${errorMessage}\n\nPlease check your data and try again.`; + setGenerationError(errorWithSuggestion); + setIsGenerated(false); + console.error('Schedule generation error:', error); + } finally { + setIsGenerating(false); + } + }; + + const renderContent = () => { + switch (currentView) { + case ViewMode.DASHBOARD: + return ( + + ); + case ViewMode.DATA: + return ( + + ); + case ViewMode.SCHEDULE: + return ( + + ); + case ViewMode.SETTINGS: + return ; + default: + return
View not found
; + } + }; + + return ( + +
+ +
+
+

+ {currentView === ViewMode.DASHBOARD && t('common.dashboard')} + {currentView === ViewMode.DATA && t('common.dataManagement')} + {currentView === ViewMode.SCHEDULE && t('common.scheduleView')} + {currentView === ViewMode.SETTINGS && t('common.settings')} +

+ +
+
+ {renderContent()} +
+ + {showConstraintModal && ( + setShowConstraintModal(false)} + onGenerate={handleFinalizeSchedule} + /> + )} + + setShowHelpModal(false)} + currentView={currentView} + /> +
+
+
+ ); +}; + +export default App; \ No newline at end of file diff --git a/.history/src/backend/services/schedulerService_20251219222058.ts b/.history/src/backend/services/schedulerService_20251219222058.ts new file mode 100644 index 0000000..545a4c3 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251219222058.ts @@ -0,0 +1,1030 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + if (classroom.capacity < course.enrolledStudents) { + return false; + } + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver: ${courses.length} courses, ${timeSlots.length} slots, ${classrooms.length} rooms`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let assigned = false; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (assigned) break; + + // Try each classroom + for (const classroom of classrooms) { + if (isSafe(course, classroom, timeSlot, context)) { + // Make the assignment + context.assignments.push({ + course, + classroom, + timeSlot + }); + assigned = true; + break; + } + } + } + + if (!assigned) { + // Could not find a valid slot for this course - analyze why + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${i + 1}/${courses.length}: ${course.code} (${course.enrolledStudents} students)`); + console.log(` Root cause: ${failureReason.rootCause}`); + return { success: false, failureReason }; + } + + // Progress log every 20 courses + if ((i + 1) % 20 === 0) { + console.log(` Progress: ${i + 1}/${courses.length} courses scheduled`); + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; // All courses scheduled +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220161902.ts b/.history/src/backend/services/schedulerService_20251220161902.ts new file mode 100644 index 0000000..d68907f --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220161902.ts @@ -0,0 +1,1028 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver: ${courses.length} courses, ${timeSlots.length} slots, ${classrooms.length} rooms`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let assigned = false; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (assigned) break; + + // Try each classroom + for (const classroom of classrooms) { + if (isSafe(course, classroom, timeSlot, context)) { + // Make the assignment + context.assignments.push({ + course, + classroom, + timeSlot + }); + assigned = true; + break; + } + } + } + + if (!assigned) { + // Could not find a valid slot for this course - analyze why + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${i + 1}/${courses.length}: ${course.code} (${course.enrolledStudents} students)`); + console.log(` Root cause: ${failureReason.rootCause}`); + return { success: false, failureReason }; + } + + // Progress log every 20 courses + if ((i + 1) % 20 === 0) { + console.log(` Progress: ${i + 1}/${courses.length} courses scheduled`); + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; // All courses scheduled +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220161938.ts b/.history/src/backend/services/schedulerService_20251220161938.ts new file mode 100644 index 0000000..403f9c9 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220161938.ts @@ -0,0 +1,1055 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Bu saat dilimi için öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: Pratik olması için ilk müsait odayla 'isSafe' kontrolü yapıyoruz, çünkü isSafe içindeki + // öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + // Eğer öğrencilerin bu saatte başka sınavı varsa, odaya bakmaya gerek yok. + continue; + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul ve Kapasiteye göre (Büyük -> Küçük) sırala + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); // En büyük odayı önce dene + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT LOGIC) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı listeye ekle ve kapasitesini düş + roomsToUse.push(room); + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer kalan öğrenci yoksa (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220161956.ts b/.history/src/backend/services/schedulerService_20251220161956.ts new file mode 100644 index 0000000..79ec3ef --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220161956.ts @@ -0,0 +1,1052 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (placed) break; + + + if (!isSafe(course, classrooms[0], timeSlot, context)) { + + continue; + } + + + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + + roomsToUse.push(room); + studentsRemaining -= room.capacity; + } + + + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220170815.ts b/.history/src/backend/services/schedulerService_20251220170815.ts new file mode 100644 index 0000000..754689c --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220170815.ts @@ -0,0 +1,1053 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (placed) break; + + + if (!isSafe(course, classrooms[0], timeSlot, context)) { + + continue; + } + + + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + + roomsToUse.push(room); + studentsRemaining -= room.capacity; + } + + + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220171037.ts b/.history/src/backend/services/schedulerService_20251220171037.ts new file mode 100644 index 0000000..b694304 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220171037.ts @@ -0,0 +1,1056 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Check for timeout (time-based, not iteration-based) + if (Date.now() - startTime > timeoutMs) { + return false; // Stop trying - timeout reached + } + + // Base case: All courses have been scheduled + if (index === courses.length) { + return true; + } + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + // Check for timeout in inner loop + if (Date.now() - startTime > timeoutMs) { + return false; + } + + // Try each classroom + for (const classroom of classrooms) { + // Check if this assignment satisfies all constraints + if (isSafe(currentCourse, classroom, timeSlot, context)) { + // Make the assignment + const assignment: ScheduleAssignment = { + course: currentCourse, + classroom, + timeSlot + }; + context.assignments.push(assignment); + + // Recursively try to schedule the remaining courses + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Remove the assignment and try another option + context.assignments.pop(); + } + } + } + + // No valid assignment found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220172612.ts b/.history/src/backend/services/schedulerService_20251220172612.ts new file mode 100644 index 0000000..e8e1cd6 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220172612.ts @@ -0,0 +1,1088 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220172741.ts b/.history/src/backend/services/schedulerService_20251220172741.ts new file mode 100644 index 0000000..0bbbd0c --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220172741.ts @@ -0,0 +1,1089 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount: number; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220172749.ts b/.history/src/backend/services/schedulerService_20251220172749.ts new file mode 100644 index 0000000..0bbbd0c --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220172749.ts @@ -0,0 +1,1089 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount: number; +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220172809.ts b/.history/src/backend/services/schedulerService_20251220172809.ts new file mode 100644 index 0000000..70cfced --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220172809.ts @@ -0,0 +1,1089 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220173731.ts b/.history/src/backend/services/schedulerService_20251220173731.ts new file mode 100644 index 0000000..0d967f4 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220173731.ts @@ -0,0 +1,1090 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220173812.ts b/.history/src/backend/services/schedulerService_20251220173812.ts new file mode 100644 index 0000000..04df24c --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220173812.ts @@ -0,0 +1,1093 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + if (classroom.capacity < course.enrolledStudents) { + return false; + } + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220173836.ts b/.history/src/backend/services/schedulerService_20251220173836.ts new file mode 100644 index 0000000..a3d3986 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220173836.ts @@ -0,0 +1,1091 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // Timeout Check + if (Date.now() - startTime > timeoutMs) return false; + + // Base Case: All courses scheduled + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + // Try each time slot + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Check Student Constraints FIRST (Optimization) + // If students are busy in this slot, don't bother checking rooms + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Find Available Rooms in this Slot + // Identify rooms NOT used in this slot by previous recursive steps + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Sort available rooms: Largest Capacity First (Best for filling large courses) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Try to Split the Course + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Fill rooms until students are covered + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + + studentsRemaining -= count; + } + + // 4. If we successfully found room(s) for ALL students in this slot + if (studentsRemaining <= 0) { + // Commit Assignments + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // RECURSE: Try to schedule the NEXT course + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Found a valid full schedule! + } + + // BACKTRACK: If next steps failed, undo these assignments and try next slot + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // No valid slot combination found for this course + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220173920.ts b/.history/src/backend/services/schedulerService_20251220173920.ts new file mode 100644 index 0000000..69395f5 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220173920.ts @@ -0,0 +1,1076 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; + + const currentCourse = courses[index]; + + for (const timeSlot of timeSlots) { + if (Date.now() - startTime > timeoutMs) return false; + + // 1. Öğrenci Kontrolü + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 2. Müsait Odalar + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. Bölme İşlemi (Split) + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + const count = Math.min(studentsRemaining, room.capacity); + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + studentsRemaining -= count; + } + + // 4. Eğer Sığdıysa + if (studentsRemaining <= 0) { + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + // Backtrack: Hepsini Geri Al + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220174411.ts b/.history/src/backend/services/schedulerService_20251220174411.ts new file mode 100644 index 0000000..9c1a148 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220174411.ts @@ -0,0 +1,1099 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + * Strategy: Largest-Fit (Fills largest available rooms first). + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // 1. Timeout ve Base Case Kontrolleri + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; // Tüm dersler atandıysa başarı! + + const currentCourse = courses[index]; + + // 2. Her zaman dilimini sırayla dene + for (const timeSlot of timeSlots) { + // Döngü içi timeout kontrolü (Büyük veride donmayı önler) + if (Date.now() - startTime > timeoutMs) return false; + + // 3. Önce Öğrenci Çakışmasını Kontrol Et (Optimization) + // Eğer öğrencilerin bu saatte başka sınavı varsa, boş oda aramaya gerek yok. + // Not: classrooms[0] dummy olarak verilir, isSafe içinde oda ID'sine bakılmaz (split için). + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 4. Bu saatteki Müsait Odaları Bul + // Daha önce atanmış (dolu) odaların ID'lerini al + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları filtrele ve KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala + // Bu "Largest Fit" stratejisidir. 150 kişilik ders için önce 100'lük, sonra 50'lik odayı seçer. + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 5. Dersi Odalara Bölüştür (Split Logic) + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Odalar yettiği sürece doldur + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odaya sığacak kadarını al (Oda kapasitesi mi, kalan öğrenci mi daha az?) + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count // <--- Kritik Nokta: Parçalı öğrenci sayısı + }); + + studentsRemaining -= count; + } + + // 6. Eğer TÜM öğrenciler yerleştiyse (studentsRemaining <= 0) + if (studentsRemaining <= 0) { + // a) Atamaları Yap (Commit) + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // b) Bir Sonraki Dersi Çözmeyi Dene (Recursive Call) + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Zincirleme başarı! + } + + // c) Başarısız Olduysa Geri Al (Backtrack) + // Eklediğimiz parça sayısı kadar pop yapıyoruz. + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // Bu ders için hiçbir zaman diliminde uygun yer bulunamadı + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220174712.ts b/.history/src/backend/services/schedulerService_20251220174712.ts new file mode 100644 index 0000000..f7cb1d4 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220174712.ts @@ -0,0 +1,1100 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + * Strategy: Largest-Fit (Fills largest available rooms first). + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // 1. Timeout ve Base Case Kontrolleri + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; // Tüm dersler atandıysa başarı! + + const currentCourse = courses[index]; + + // 2. Her zaman dilimini sırayla dene + for (const timeSlot of timeSlots) { + // Döngü içi timeout kontrolü (Büyük veride donmayı önler) + if (Date.now() - startTime > timeoutMs) return false; + + // 3. Önce Öğrenci Çakışmasını Kontrol Et (Optimization) + // Eğer öğrencilerin bu saatte başka sınavı varsa, boş oda aramaya gerek yok. + // Not: classrooms[0] dummy olarak verilir, isSafe içinde oda ID'sine bakılmaz (split için). + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 4. Bu saatteki Müsait Odaları Bul + // Daha önce atanmış (dolu) odaların ID'lerini al + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları filtrele ve KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala + // Bu "Largest Fit" stratejisidir. 150 kişilik ders için önce 100'lük, sonra 50'lik odayı seçer. + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 5. Dersi Odalara Bölüştür (Split Logic) + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Odalar yettiği sürece doldur + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odaya sığacak kadarını al (Oda kapasitesi mi, kalan öğrenci mi daha az?) + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count // <--- Kritik Nokta: Parçalı öğrenci sayısı + }); + + studentsRemaining -= count; + } + + // 6. Eğer TÜM öğrenciler yerleştiyse (studentsRemaining <= 0) + if (studentsRemaining <= 0) { + // a) Atamaları Yap (Commit) + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // b) Bir Sonraki Dersi Çözmeyi Dene (Recursive Call) + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Zincirleme başarı! + } + + // c) Başarısız Olduysa Geri Al (Backtrack) + // Eklediğimiz parça sayısı kadar pop yapıyoruz. + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // Bu ders için hiçbir zaman diliminde uygun yer bulunamadı + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot, + studentCount: number + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251220174738.ts b/.history/src/backend/services/schedulerService_20251220174738.ts new file mode 100644 index 0000000..4f66b78 --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251220174738.ts @@ -0,0 +1,1100 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // Priority 1: Capacity issue (no room can fit) + if (roomsWithCapacity.length === 0) { + rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxRoomCapacity + } + }); + } + // Priority 2: Student conflicts blocking all slots + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add a room with ${course.enrolledStudents}+ seats`, + impact: `Will immediately allow scheduling ${course.code}` + }); + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + * Strategy: Largest-Fit (Fills largest available rooms first). + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // 1. Timeout ve Base Case Kontrolleri + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; // Tüm dersler atandıysa başarı! + + const currentCourse = courses[index]; + + // 2. Her zaman dilimini sırayla dene + for (const timeSlot of timeSlots) { + // Döngü içi timeout kontrolü (Büyük veride donmayı önler) + if (Date.now() - startTime > timeoutMs) return false; + + // 3. Önce Öğrenci Çakışmasını Kontrol Et (Optimization) + // Eğer öğrencilerin bu saatte başka sınavı varsa, boş oda aramaya gerek yok. + // Not: classrooms[0] dummy olarak verilir, isSafe içinde oda ID'sine bakılmaz (split için). + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 4. Bu saatteki Müsait Odaları Bul + // Daha önce atanmış (dolu) odaların ID'lerini al + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları filtrele ve KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala + // Bu "Largest Fit" stratejisidir. 150 kişilik ders için önce 100'lük, sonra 50'lik odayı seçer. + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 5. Dersi Odalara Bölüştür (Split Logic) + let studentsRemaining = currentCourse.enrolledStudents; + const potentialAssignments: ScheduleAssignment[] = []; + + // Odalar yettiği sürece doldur + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odaya sığacak kadarını al (Oda kapasitesi mi, kalan öğrenci mi daha az?) + const count = Math.min(studentsRemaining, room.capacity); + + potentialAssignments.push({ + course: currentCourse, + classroom: room, + timeSlot: timeSlot, + studentCount: count // <--- Kritik Nokta: Parçalı öğrenci sayısı + }); + + studentsRemaining -= count; + } + + // 6. Eğer TÜM öğrenciler yerleştiyse (studentsRemaining <= 0) + if (studentsRemaining <= 0) { + // a) Atamaları Yap (Commit) + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + // b) Bir Sonraki Dersi Çözmeyi Dene (Recursive Call) + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; // Zincirleme başarı! + } + + // c) Başarısız Olduysa Geri Al (Backtrack) + // Eklediğimiz parça sayısı kadar pop yapıyoruz. + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + // Bu ders için hiçbir zaman diliminde uygun yer bulunamadı + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + + for (const room of availableRooms) { + if (studentsRemaining <= 0) break; + + // Odayı kullan listesine al + roomsToUse.push(room); + // Öğrencileri bu odaya doldur + studentsRemaining -= room.capacity; + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining <= 0) { + roomsToUse.forEach(room => { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot, + studentCount: room.capacity + }); + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.name, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime + })); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/backend/services/schedulerService_20251221142457.ts b/.history/src/backend/services/schedulerService_20251221142457.ts new file mode 100644 index 0000000..ab66e4c --- /dev/null +++ b/.history/src/backend/services/schedulerService_20251221142457.ts @@ -0,0 +1,1199 @@ +/** + * Exam Scheduling Engine + * + * Implements a Constraint Satisfaction Problem (CSP) solver using + * Recursive Backtracking with the Degree Heuristic for exam scheduling. + */ + +import type { Course, Classroom, Student, ExamSession, GenerationConstraints } from '../../types'; + +// ============================================================================ +// Types +// ============================================================================ + +interface TimeSlot { + id: string; + date: Date; + startTime: Date; + endTime: Date; + dayIndex: number; // 0-based day number in the exam period + slotIndex: number; // 0-based slot number within the day +} + +interface ScheduleAssignment { + course: Course; + classroom: Classroom; + timeSlot: TimeSlot; + studentCount:number; + +} + +/** + * Detailed reason why a course could not be scheduled + */ +interface ConstraintViolation { + type: 'capacity' | 'room_conflict' | 'student_conflict' | 'consecutive_exam' | 'max_daily_exams' | 'min_hours_gap'; + severity: 'blocking' | 'contributing'; + description: string; + details: { + affectedStudentCount?: number; + sampleStudents?: string[]; + conflictingCourses?: string[]; + blockedSlots?: number; + requiredCapacity?: number; + maxAvailableCapacity?: number; + }; +} + +interface FailureReason { + courseCode: string; + courseEnrollment: number; + courseIndex: number; + totalCourses: number; + rootCause: string; + rootCauseType: ConstraintViolation['type']; + constraintViolations: ConstraintViolation[]; + suggestions: { + priority: 'high' | 'medium' | 'low'; + action: string; + impact: string; + }[]; + diagnostics: { + testedSlots: number; + testedRooms: number; + scheduledSoFar: number; + availableSlots: number; + availableRooms: number; + slotsBlockedByStudentConflicts: number; + slotsBlockedByRoomConflicts: number; + roomsBlockedByCapacity: number; + }; +} + +interface ScheduleResult { + success: boolean; + schedule: ExamSession[]; + message: string; + failureDetails?: FailureReason; + stats?: { + totalCourses: number; + scheduledCourses: number; + totalTimeSlots: number; + totalClassrooms: number; + generationTimeMs: number; + }; +} + +// ============================================================================ +// Configuration Constants +// ============================================================================ + +const EXAM_DURATION_HOURS = 2; +const BREAK_BETWEEN_EXAMS_HOURS = 1; +const MAX_EXAMS_PER_DAY_PER_STUDENT = 2; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Formats a Date as a local ISO string (preserves local timezone). + * This prevents the UTC conversion that happens with toISOString(). + */ +function toLocalISOString(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`; +} + +/** + * Generates all available time slots based on the given constraints. + * The break between slots is determined by minHoursBetweenExams (defaults to 1 hour). + */ +function generateTimeSlots(constraints: GenerationConstraints): TimeSlot[] { + const slots: TimeSlot[] = []; + + const startDate = new Date(constraints.startDate); + const endDate = new Date(constraints.endDate); + + // Parse daily time boundaries + const [startHour, startMinute] = constraints.dailyStartTime.split(':').map(Number); + const [endHour, endMinute] = constraints.dailyEndTime.split(':').map(Number); + + // Use the user's minHoursBetweenExams as the break between slots (minimum 1 hour for logistics) + const breakBetweenSlots = Math.max(1, constraints.minHoursBetweenExams ?? 1); + + let dayIndex = 0; + const currentDate = new Date(startDate); + + while (currentDate <= endDate) { + const dayOfWeek = currentDate.getDay(); + + // Skip weekends if not included + if (!constraints.includeWeekends && (dayOfWeek === 0 || dayOfWeek === 6)) { + currentDate.setDate(currentDate.getDate() + 1); + continue; + } + + // Generate slots for this day + let slotIndex = 0; + let currentHour = startHour; + let currentMinute = startMinute; + + while (currentHour + EXAM_DURATION_HOURS <= endHour || + (currentHour + EXAM_DURATION_HOURS === endHour && currentMinute <= endMinute)) { + + const slotStart = new Date(currentDate); + slotStart.setHours(currentHour, currentMinute, 0, 0); + + const slotEnd = new Date(slotStart); + slotEnd.setHours(slotStart.getHours() + EXAM_DURATION_HOURS); + + slots.push({ + id: `day${dayIndex}_slot${slotIndex}`, + date: new Date(currentDate), + startTime: slotStart, + endTime: slotEnd, + dayIndex, + slotIndex + }); + + // Move to next slot (exam duration + user-defined break) + currentHour += EXAM_DURATION_HOURS + breakBetweenSlots; + slotIndex++; + } + + currentDate.setDate(currentDate.getDate() + 1); + dayIndex++; + } + + return slots; +} + +/** + * Sorts courses by difficulty using the Degree Heuristic. + * Courses with more enrolled students are scheduled first (harder to place). + */ +function sortCoursesByDifficulty(courses: Course[]): Course[] { + return [...courses].sort((a, b) => b.enrolledStudents - a.enrolledStudents); +} + +/** + * Builds a lookup map: studentId -> list of courseIds they're enrolled in + */ +function buildStudentCourseMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + map.set(student.id, [...student.enrolledCourses]); + } + return map; +} + +/** + * Builds a lookup map: courseId -> list of studentIds enrolled in it + */ +function buildCourseStudentMap(students: Student[]): Map { + const map = new Map(); + for (const student of students) { + for (const courseId of student.enrolledCourses) { + if (!map.has(courseId)) { + map.set(courseId, []); + } + map.get(courseId)!.push(student.id); + } + } + return map; +} + +// ============================================================================ +// Constraint Validation +// ============================================================================ + +interface ValidationContext { + assignments: ScheduleAssignment[]; + courseStudentMap: Map; + studentCourseMap: Map; + maxExamsPerDay: number; + allowConsecutiveExams: boolean; + minHoursBetweenExams: number; +} + +/** + * Checks if a course can be assigned to a specific classroom and time slot. + * Returns true if all constraints are satisfied. + */ +function isSafe( + course: Course, + classroom: Classroom, + timeSlot: TimeSlot, + context: ValidationContext +): boolean { + // Constraint 1: Capacity Check + // Room must have enough capacity for all enrolled students + + + + // Constraint 2: Room Availability Check + // The classroom must not be already booked at this time + const roomConflict = context.assignments.some( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + return false; + } + + // Get students enrolled in this course + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + + for (const studentId of studentsInCourse) { + // Constraint 3: No Student Conflicts + // A student cannot have two exams at the same time slot + const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + return false; + } + + // Constraint 4: No Consecutive Exams (configurable) + // If disabled, students cannot have exams in consecutive time slots + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + if (a.timeSlot.dayIndex === timeSlot.dayIndex) { + const slotDiff = Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex); + return slotDiff === 1; + } + return false; + }); + if (consecutiveConflict) { + return false; + } + } + + // Constraint 5: Maximum Daily Exams (configurable) + // A student cannot have more than maxExamsPerDay exams on the same day + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && + a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + + if (examsOnSameDay >= context.maxExamsPerDay) { + return false; + } + + // Constraint 6: Minimum Hours Between Exams (configurable) + // Ensure there's at least minHoursBetweenExams gap between exams for same student + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; // Convert hours to ms + const tooCloseExam = context.assignments.some((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) { + return false; + } + // Check if exams are too close in time + const examEnd = a.timeSlot.endTime.getTime(); + const newExamStart = timeSlot.startTime.getTime(); + const existingExamStart = a.timeSlot.startTime.getTime(); + const newExamEnd = timeSlot.endTime.getTime(); + + // Check gap in both directions + const gapAfterExisting = newExamStart - examEnd; + const gapAfterNew = existingExamStart - newExamEnd; + + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + return false; + } + } + } + + return true; +} + +/** + * Analyzes why a course cannot be scheduled in any slot. + * Returns detailed information about constraint violations with root cause analysis. + */ +function getFailureReason( + course: Course, + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + courseIndex: number = 0, + totalCourses: number = 0 +): FailureReason { + const violations: ConstraintViolation[] = []; + + // Diagnostic counters + let slotsBlockedByStudentConflicts = 0; + let slotsBlockedByRoomConflicts = 0; + let roomsBlockedByCapacity = 0; + + // Track different failure types with details + const capacityIssue = { rooms: [] as string[], maxCapacity: 0 }; + const studentConflictMap = new Map; slots: number }>(); + const maxDailyIssue = { affectedStudents: new Set(), blockedDays: new Set() }; + const minHoursIssue = { affectedStudents: new Set(), courses: new Set() }; + const consecutiveIssue = { affectedStudents: new Set(), courses: new Set() }; + + const studentsInCourse = context.courseStudentMap.get(course.id) || []; + const roomsWithCapacity = classrooms.filter(c => c.capacity >= course.enrolledStudents); + + // Calculate capacity stats + const maxRoomCapacity = Math.max(...classrooms.map(c => c.capacity), 0); + roomsBlockedByCapacity = classrooms.length - roomsWithCapacity.length; + + // Analyze each slot + for (const timeSlot of timeSlots) { + for (const classroom of roomsWithCapacity) { + // Check room already booked + const roomConflict = context.assignments.find( + (a) => a.classroom.id === classroom.id && a.timeSlot.id === timeSlot.id + ); + if (roomConflict) { + slotsBlockedByRoomConflicts++; + continue; + } + + // Check student conflicts (most important) + let slotBlocked = false; + for (const studentId of studentsInCourse) { + // Same time conflict + const sameTimeConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; + }); + if (sameTimeConflict) { + if (!studentConflictMap.has(sameTimeConflict.course.code)) { + studentConflictMap.set(sameTimeConflict.course.code, { students: new Set(), slots: 0 }); + } + const entry = studentConflictMap.get(sameTimeConflict.course.code)!; + entry.students.add(studentId); + entry.slots++; + slotsBlockedByStudentConflicts++; + slotBlocked = true; + break; + } + + // Max daily exams + const examsOnSameDay = context.assignments.filter((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + return studentsInAssignedCourse.includes(studentId) && a.timeSlot.dayIndex === timeSlot.dayIndex; + }).length; + if (examsOnSameDay >= context.maxExamsPerDay) { + maxDailyIssue.affectedStudents.add(studentId); + maxDailyIssue.blockedDays.add(timeSlot.dayIndex); + slotBlocked = true; + break; + } + + // Consecutive exams + if (!context.allowConsecutiveExams) { + const consecutiveConflict = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + return a.timeSlot.dayIndex === timeSlot.dayIndex && + Math.abs(a.timeSlot.slotIndex - timeSlot.slotIndex) === 1; + }); + if (consecutiveConflict) { + consecutiveIssue.affectedStudents.add(studentId); + consecutiveIssue.courses.add(consecutiveConflict.course.code); + slotBlocked = true; + break; + } + } + + // Min hours gap + if (context.minHoursBetweenExams > 0) { + const minGapMs = context.minHoursBetweenExams * 60 * 60 * 1000; + const tooCloseExam = context.assignments.find((a) => { + const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; + if (!studentsInAssignedCourse.includes(studentId)) return false; + const gapAfterExisting = timeSlot.startTime.getTime() - a.timeSlot.endTime.getTime(); + const gapAfterNew = a.timeSlot.startTime.getTime() - timeSlot.endTime.getTime(); + return (gapAfterExisting >= 0 && gapAfterExisting < minGapMs) || + (gapAfterNew >= 0 && gapAfterNew < minGapMs); + }); + if (tooCloseExam) { + minHoursIssue.affectedStudents.add(studentId); + minHoursIssue.courses.add(tooCloseExam.course.code); + slotBlocked = true; + break; + } + } + } + } + } + + // Determine root cause and build violations + let rootCause = ''; + let rootCauseType: ConstraintViolation['type'] = 'capacity'; + + // NEW Priority 1: Insufficient TOTAL capacity across rooms, even with splitting + let hasAnySlotEnoughTotalCapacity = false; + let maxPossibleCapacityInAnySingleSlot = 0; + + for (const timeSlot of timeSlots) { + const occupiedRoomIdsAtSlot = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const totalUnoccupiedCapacityAtSlot = classrooms + .filter(r => !occupiedRoomIdsAtSlot.has(r.id)) + .reduce((sum, room) => sum + room.capacity, 0); + + if (totalUnoccupiedCapacityAtSlot >= course.enrolledStudents) { + hasAnySlotEnoughTotalCapacity = true; + break; + } + maxPossibleCapacityInAnySingleSlot = Math.max(maxPossibleCapacityInAnySingleSlot, totalUnoccupiedCapacityAtSlot); + } + + if (!hasAnySlotEnoughTotalCapacity) { + rootCause = `INSUFFICIENT TOTAL CAPACITY: Course has ${course.enrolledStudents} students. No single time slot has enough total available classroom capacity (max ${maxPossibleCapacityInAnySingleSlot}) to accommodate all students, even with splitting across multiple rooms.`; + rootCauseType = 'capacity'; + violations.push({ + type: 'capacity', + severity: 'blocking', + description: `Required total capacity: ${course.enrolledStudents}. Maximum available total capacity in any single time slot: ${maxPossibleCapacityInAnySingleSlot}.`, + details: { + requiredCapacity: course.enrolledStudents, + maxAvailableCapacity: maxPossibleCapacityInAnySingleSlot + } + }); + } + // Original Priority 1 becomes Priority 2 if the new total capacity check passes + else if (studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + const [conflictCourse, data] = topConflict; + rootCause = `STUDENT CONFLICT: ${data.students.size} students share both ${course.code} and ${conflictCourse}. All available slots are blocked.`; + rootCauseType = 'student_conflict'; + + studentConflictMap.forEach((data, conflictCourse) => { + violations.push({ + type: 'student_conflict', + severity: data.slots > timeSlots.length / 2 ? 'blocking' : 'contributing', + description: `${data.students.size} students also enrolled in ${conflictCourse}`, + details: { + affectedStudentCount: data.students.size, + sampleStudents: [...data.students].slice(0, 3), + conflictingCourses: [conflictCourse], + blockedSlots: data.slots + } + }); + }); + } + // Priority 3: Max daily exams + else if (maxDailyIssue.affectedStudents.size > 0) { + rootCause = `MAX DAILY LIMIT: ${maxDailyIssue.affectedStudents.size} students already have ${context.maxExamsPerDay} exams on ${maxDailyIssue.blockedDays.size} day(s).`; + rootCauseType = 'max_daily_exams'; + violations.push({ + type: 'max_daily_exams', + severity: 'blocking', + description: `Daily limit of ${context.maxExamsPerDay} exams reached for students`, + details: { + affectedStudentCount: maxDailyIssue.affectedStudents.size, + sampleStudents: [...maxDailyIssue.affectedStudents].slice(0, 3), + blockedSlots: maxDailyIssue.blockedDays.size * (timeSlots.length / (new Set(timeSlots.map(t => t.dayIndex)).size || 1)) + } + }); + } + // Priority 4: Min hours gap + else if (minHoursIssue.affectedStudents.size > 0) { + rootCause = `TIME GAP VIOLATION: ${context.minHoursBetweenExams}h gap required but conflicts with ${[...minHoursIssue.courses].join(', ')}.`; + rootCauseType = 'min_hours_gap'; + violations.push({ + type: 'min_hours_gap', + severity: 'blocking', + description: `Need ${context.minHoursBetweenExams}h gap between exams`, + details: { + affectedStudentCount: minHoursIssue.affectedStudents.size, + sampleStudents: [...minHoursIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...minHoursIssue.courses] + } + }); + } + // Priority 5: Consecutive exams + else if (consecutiveIssue.affectedStudents.size > 0) { + rootCause = `CONSECUTIVE EXAM BAN: Would place back-to-back with ${[...consecutiveIssue.courses].join(', ')}.`; + rootCauseType = 'consecutive_exam'; + violations.push({ + type: 'consecutive_exam', + severity: 'blocking', + description: 'Consecutive exams not allowed for same student', + details: { + affectedStudentCount: consecutiveIssue.affectedStudents.size, + sampleStudents: [...consecutiveIssue.affectedStudents].slice(0, 3), + conflictingCourses: [...consecutiveIssue.courses] + } + }); + } + // Fallback: General resource constraint + else { + rootCause = `RESOURCE EXHAUSTED: All time/room combinations are exhausted for ${course.code}.`; + rootCauseType = 'room_conflict'; + violations.push({ + type: 'room_conflict', + severity: 'blocking', + description: 'All available time/room combinations are already used or blocked by other constraints', + details: { blockedSlots: slotsBlockedByRoomConflicts } + }); + } + + // Generate precise suggestions based on root cause + const suggestions: FailureReason['suggestions'] = []; + + if (rootCauseType === 'capacity') { + suggestions.push({ + priority: 'high', + action: `Add more classrooms or increase capacity of existing ones.`, + impact: `Will immediately provide more total seating capacity for ${course.code}` + }); + if (maxPossibleCapacityInAnySingleSlot > 0) { + suggestions.push({ + priority: 'medium', + action: `Consider reducing enrollment for ${course.code} by ${course.enrolledStudents - maxPossibleCapacityInAnySingleSlot} students.`, + impact: `May allow course to fit into existing capacity.` + }); + } + } + + if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { + const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; + suggestions.push({ + priority: 'high', + action: `Add ${Math.ceil(timeSlots.length * 0.3)} more time slots (extend exam period by 1-2 days)`, + impact: `Creates room for ${course.code} and ${topConflict[0]} at different times` + }); + suggestions.push({ + priority: 'medium', + action: `Add ${Math.ceil(roomsWithCapacity.length * 0.5)} more large classrooms`, + impact: 'Enables parallel scheduling of conflicting courses' + }); + } + + if (rootCauseType === 'max_daily_exams') { + suggestions.push({ + priority: 'high', + action: `Increase max exams per day from ${context.maxExamsPerDay} to ${context.maxExamsPerDay + 1}`, + impact: `Frees up ${maxDailyIssue.blockedDays.size} days for ${course.code}` + }); + suggestions.push({ + priority: 'medium', + action: 'Extend exam period by 2+ days', + impact: `Spreads exams over more days for ${maxDailyIssue.affectedStudents.size} students` + }); + } + + if (rootCauseType === 'min_hours_gap') { + suggestions.push({ + priority: 'high', + action: `Reduce minimum hours gap from ${context.minHoursBetweenExams}h to ${Math.max(0, context.minHoursBetweenExams - 1)}h`, + impact: `Opens ${Math.ceil(timeSlots.length * 0.2)} more slot combinations` + }); + } + + if (rootCauseType === 'consecutive_exam') { + suggestions.push({ + priority: 'high', + action: 'Allow consecutive exams (enable in settings)', + impact: `Unblocks slots adjacent to ${[...consecutiveIssue.courses].join(', ')}` + }); + } + + return { + courseCode: course.code, + courseEnrollment: course.enrolledStudents, + courseIndex, + totalCourses, + rootCause, + rootCauseType, + constraintViolations: violations, + suggestions, + diagnostics: { + testedSlots: timeSlots.length, + testedRooms: roomsWithCapacity.length * timeSlots.length, + scheduledSoFar: context.assignments.length, + availableSlots: timeSlots.length, + availableRooms: roomsWithCapacity.length, + slotsBlockedByStudentConflicts, + slotsBlockedByRoomConflicts, + roomsBlockedByCapacity + } + }; +} + +// ============================================================================ +// Backtracking Solver +// ============================================================================ + +/** + * Recursively attempts to schedule all courses using backtracking. + * + * @param courses - List of courses to schedule (sorted by difficulty) + * @param classrooms - Available classrooms + * @param timeSlots - Available time slots + * @param context - Validation context with current assignments + * @param index - Current index in the courses array + * @param iterationCount - Reference object to track iterations + * @param maxIterations - Maximum allowed iterations before timeout + * @returns true if a valid schedule was found, false otherwise + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + * Strategy: Largest-Fit (Fills largest available rooms first). + */ +function solve( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext, + index: number, + startTime: number, + timeoutMs: number +): boolean { + // 1. Timeout ve Base Case Kontrolleri + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; // Tüm dersler atandıysa başarı! + + const currentCourse = courses[index]; + let studentsRemainingForCurrentCourse = currentCourse.enrolledStudents; + + // 2. Her zaman dilimini sırayla dene + for (const timeSlot of timeSlots) { + // Döngü içi timeout kontrolü (Büyük veride donmayı önler) + if (Date.now() - startTime > timeoutMs) return false; + + // 3. Önce Öğrenci Çakışmasını Kontrol Et (Optimization) + // Eğer öğrencilerin bu saatte başka sınavı varsa, boş oda aramaya gerek yok. + // Not: classrooms[0] dummy olarak verilir, isSafe içinde oda ID'sine bakılmaz (split için). + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; + } + + // 4. Bu saatteki Müsait Odaları Bul + // Daha önce atanmış (dolu) odaların ID'lerini al + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları filtrele ve KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala + // Bu "Largest Fit" stratejisidir. 150 kişilik ders için önce 100'lük, sonra 50'lik odayı seçer. + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) // Only consider rooms that are not occupied + .sort((a, b) => b.capacity - a.capacity); // Sort by capacity descending (Largest Fit) + + // 5. Dersi Odalara Bölüştür (Split Logic) + let studentsRemaining = studentsRemainingForCurrentCourse; // Use a local variable for splitting logic + const potentialAssignments: ScheduleAssignment[] = []; + const roomsUsedInCurrentSplit = new Set(); // Keep track of rooms already used for this course split + + while (studentsRemaining > 0) { + // Get rooms that are available (not occupied by another course in this time slot) + // and have not yet been used for *this* course's split. + const currentAvailableRooms = classrooms.filter(r => + !occupiedRoomIds.has(r.id) && !roomsUsedInCurrentSplit.has(r.id) + ); + + if (currentAvailableRooms.length === 0) { + studentsRemaining = -1; // Indicate failure to place all students + break; + } + + // --- Strategy: Prioritize Best Fit for remaining students --- + let bestFitRoom: Classroom | null = null; + let bestFitCapacityDiff = Infinity; // capacity - studentsRemaining, want smallest non-negative difference + + for (const room of currentAvailableRooms) { + if (room.capacity >= studentsRemaining) { // Can this room fit all remaining students? + const diff = room.capacity - studentsRemaining; + if (diff < bestFitCapacityDiff) { // Is it a better fit (smaller difference)? + bestFitCapacityDiff = diff; + bestFitRoom = room; + } + } + } + + if (bestFitRoom) { // Found a room that can fit all remaining students (Best Fit) + potentialAssignments.push({ + course: currentCourse, + classroom: bestFitRoom, + timeSlot: timeSlot, + studentCount: studentsRemaining + }); + roomsUsedInCurrentSplit.add(bestFitRoom.id); + studentsRemaining = 0; // All students placed + } else { // No single room can fit all remaining students, use Largest Fit strategy for current chunk + // Find the largest available and unused room + const largestRoom = currentAvailableRooms.sort((a, b) => b.capacity - a.capacity)[0]; + + const count = Math.min(studentsRemaining, largestRoom.capacity); + potentialAssignments.push({ + course: currentCourse, + classroom: largestRoom, + timeSlot: timeSlot, + studentCount: count + }); + roomsUsedInCurrentSplit.add(largestRoom.id); + studentsRemaining -= count; + } + } + + + if (studentsRemaining === 0) { + + for (const assignment of potentialAssignments) { + context.assignments.push(assignment); + } + + + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } + + for (let i = 0; i < potentialAssignments.length; i++) { + context.assignments.pop(); + } + } + } + + return false; +} + +/** + * Greedy scheduling algorithm - faster for large datasets. + * Assigns each course to the first valid time slot/room combination. + * O(courses * timeSlots * classrooms) - much faster than backtracking. + * Returns both success status and detailed failure reason if failed. + */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ +function solveGreedy( + courses: Course[], + classrooms: Classroom[], + timeSlots: TimeSlot[], + context: ValidationContext +): { success: boolean; failureReason?: FailureReason } { + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); + + for (let i = 0; i < courses.length; i++) { + const course = courses[i]; + let placed = false; + + // Her zaman dilimini dene + for (const timeSlot of timeSlots) { + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + const roomsUsedInCurrentSplit = new Set(); + + while (studentsRemaining > 0) { + const currentAvailableRooms = classrooms.filter(r => + !occupiedRoomIds.has(r.id) && !roomsUsedInCurrentSplit.has(r.id) + ); + + if (currentAvailableRooms.length === 0) { + studentsRemaining = -1; // Indicate failure + break; + } + + // --- Strategy: Prioritize Best Fit for remaining students --- + let bestFitRoom: Classroom | null = null; + let bestFitCapacityDiff = Infinity; + + for (const room of currentAvailableRooms) { + if (room.capacity >= studentsRemaining) { + const diff = room.capacity - studentsRemaining; + if (diff < bestFitCapacityDiff) { + bestFitCapacityDiff = diff; + bestFitRoom = room; + } + } + } + + if (bestFitRoom) { // Found a room that can fit all remaining students (Best Fit) + roomsToUse.push(bestFitRoom); + roomsUsedInCurrentSplit.add(bestFitRoom.id); + studentsRemaining = 0; // All students placed + } else { // No single room can fit all remaining students, use Largest Fit strategy + const largestRoom = currentAvailableRooms.sort((a, b) => b.capacity - a.capacity)[0]; + + roomsToUse.push(largestRoom); + roomsUsedInCurrentSplit.add(largestRoom.id); + studentsRemaining -= largestRoom.capacity; + } + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining === 0) { + let studentsToAssign = course.enrolledStudents; + roomsToUse.forEach(room => { + const count = Math.min(studentsToAssign, room.capacity); + if (count > 0) { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + studentsToAssign -= count; + } + }); + placed = true; + } + } + + if (!placed) { + const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); + return { success: false, failureReason }; + } + } + + console.log(`SUCCESS: All ${courses.length} courses scheduled!`); + return { success: true }; +} + +// ============================================================================ +// Main Scheduling Function +// ============================================================================ + +/** + * Generates an exam schedule based on the provided data and constraints. + * + * @param courses - List of courses requiring exams + * @param classrooms - Available classrooms + * @param students - List of students with their course enrollments + * @param constraints - Scheduling constraints (dates, times, etc.) + * @returns ScheduleResult with success status and generated schedule + */ +export function generateSchedule( + courses: Course[], + classrooms: Classroom[], + students: Student[], + constraints: GenerationConstraints +): ScheduleResult { + console.log('SCHEDULER INPUT CLASSROOMS:', JSON.stringify(classrooms, null, 2)); + const startTime = Date.now(); + + // Validate input + if (courses.length === 0) { + return { + success: false, + schedule: [], + message: 'No courses provided for scheduling.' + }; + } + + if (classrooms.length === 0) { + return { + success: false, + schedule: [], + message: 'No classrooms available for scheduling.' + }; + } + + // Generate time slots + const timeSlots = generateTimeSlots(constraints); + + if (timeSlots.length === 0) { + return { + success: false, + schedule: [], + message: 'No valid time slots available. Check your date range and daily time settings.' + }; + } + + // Diagnostic: Show time slot analysis + const uniqueDays = new Set(timeSlots.map(t => t.dayIndex)).size; + const slotsPerDay = timeSlots.length / uniqueDays; + const maxParallelCapacity = timeSlots.length * classrooms.length; + console.log('========== SCHEDULING CAPACITY ANALYSIS =========='); + console.log(`📅 Days: ${uniqueDays}`); + console.log(`⏰ Slots per day: ${slotsPerDay}`); + console.log(`🏫 Total time slots: ${timeSlots.length}`); + console.log(`🚪 Classrooms: ${classrooms.length}`); + console.log(`📚 Courses to schedule: ${courses.length}`); + console.log(`📊 Max theoretical capacity: ${maxParallelCapacity} (${timeSlots.length} slots × ${classrooms.length} rooms)`); + console.log(`✅ Feasibility: ${maxParallelCapacity >= courses.length ? 'POSSIBLE' : 'IMPOSSIBLE - need more slots or rooms'}`); + console.log('=================================================='); + + // Sort courses by difficulty (Degree Heuristic) + const sortedCourses = sortCoursesByDifficulty(courses); + + // Build lookup maps for efficient constraint checking + const courseStudentMap = buildCourseStudentMap(students); + const studentCourseMap = buildStudentCourseMap(students); + + // Diagnostic: Analyze student conflicts to estimate actual capacity needed + let maxCoursesPerStudent = 0; + let studentsWithMultipleCourses = 0; + studentCourseMap.forEach((courseIds) => { + if (courseIds.length > 1) studentsWithMultipleCourses++; + maxCoursesPerStudent = Math.max(maxCoursesPerStudent, courseIds.length); + }); + + // With maxExamsPerDay constraint, a student with N courses needs at least ceil(N / maxExamsPerDay) days + const maxExamsPerDay = constraints.maxExamsPerDay ?? 2; + const minDaysNeeded = Math.ceil(maxCoursesPerStudent / maxExamsPerDay); + const minSlotsNeeded = maxCoursesPerStudent; // Each course for that student needs a different slot + + console.log('========== STUDENT CONFLICT ANALYSIS =========='); + console.log(`👥 Students with 2+ courses: ${studentsWithMultipleCourses}`); + console.log(`📚 Max courses per student: ${maxCoursesPerStudent}`); + console.log(`📅 Max exams per day allowed: ${maxExamsPerDay}`); + console.log(`⚠️ Min days needed for busiest student: ${minDaysNeeded}`); + console.log(`⚠️ Min SEQUENTIAL slots needed: ${minSlotsNeeded} (can't parallelize for same student)`); + if (minSlotsNeeded > timeSlots.length) { + console.log(`❌ PROBLEM: Need ${minSlotsNeeded} sequential slots but only have ${timeSlots.length}!`); + } + console.log('================================================'); + + // Initialize validation context with configurable constraints + const context: ValidationContext = { + assignments: [], + courseStudentMap, + studentCourseMap, + maxExamsPerDay, + allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, + minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 + }; + + + // Choose algorithm based on dataset size + // Greedy: Fast, good for large datasets (50+ courses) + // Backtracking: Slower but can find solutions where greedy fails (small datasets) + const useGreedy = courses.length >= 50; + let success: boolean; + let timedOut = false; + let failureDetails: FailureReason | undefined; + + if (useGreedy) { + console.log(`Using GREEDY algorithm for ${courses.length} courses (fast mode)`); + const greedyResult = solveGreedy(sortedCourses, classrooms, timeSlots, context); + success = greedyResult.success; + failureDetails = greedyResult.failureReason; + } else { + // Use backtracking with timeout for smaller datasets + const baseTimeout = 5000; + const scaleFactor = Math.max(1, Math.ceil(courses.length / 20)); + const timeoutMs = Math.min(baseTimeout * scaleFactor, 30000); + console.log(`Using BACKTRACKING algorithm for ${courses.length} courses (timeout: ${timeoutMs}ms)`); + success = solve(sortedCourses, classrooms, timeSlots, context, 0, startTime, timeoutMs); + timedOut = (Date.now() - startTime) >= timeoutMs; + + // If backtracking failed, get failure reason for the first unscheduled course + if (!success && sortedCourses.length > context.assignments.length) { + const failedCourseIndex = context.assignments.length; + const failedCourse = sortedCourses[failedCourseIndex]; + failureDetails = getFailureReason(failedCourse, classrooms, timeSlots, context, failedCourseIndex, sortedCourses.length); + } + } + + const endTime = Date.now(); + + if (!success) { + // Build detailed error message + let detailedMessage = timedOut + ? '⏱️ TIMEOUT: The algorithm took too long searching for a valid schedule.\n' + : '❌ SCHEDULING FAILED\n'; + + if (failureDetails) { + // Header with progress info + detailedMessage += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + detailedMessage += `📊 Progress: ${failureDetails.diagnostics.scheduledSoFar}/${failureDetails.totalCourses} courses scheduled\n`; + detailedMessage += `🚫 Failed at: ${failureDetails.courseCode} (${failureDetails.courseEnrollment} students)\n`; + detailedMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + // Root cause (the main reason) + detailedMessage += `\n🔍 ROOT CAUSE:\n`; + detailedMessage += ` ${failureDetails.rootCause}\n`; + + // Suggestions with priority + if (failureDetails.suggestions.length > 0) { + detailedMessage += `\n💡 HOW TO FIX:\n`; + failureDetails.suggestions.forEach((s, i) => { + const icon = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢'; + detailedMessage += ` ${icon} ${s.action}\n`; + detailedMessage += ` → ${s.impact}\n`; + }); + } + + // Diagnostics summary + detailedMessage += `\n📈 DIAGNOSTICS:\n`; + detailedMessage += ` • Tested: ${failureDetails.diagnostics.testedSlots} slots × ${failureDetails.diagnostics.availableRooms} rooms\n`; + detailedMessage += ` • Blocked by student conflicts: ${failureDetails.diagnostics.slotsBlockedByStudentConflicts} combinations\n`; + detailedMessage += ` • Blocked by room conflicts: ${failureDetails.diagnostics.slotsBlockedByRoomConflicts} combinations\n`; + if (failureDetails.diagnostics.roomsBlockedByCapacity > 0) { + detailedMessage += ` • Rooms too small: ${failureDetails.diagnostics.roomsBlockedByCapacity}/${classrooms.length}\n`; + } + } else { + detailedMessage += '\nNo detailed failure information available.'; + } + + return { + success: false, + schedule: [], + message: detailedMessage, + failureDetails, + stats: { + totalCourses: courses.length, + scheduledCourses: context.assignments.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; + } + + // Convert assignments to ExamSessions + // Use classroom.name as the classroomId since app.tsx maps classroom id to name + const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ + id: `exam_${idx + 1}`, + courseId: a.course.id, + classroomId: a.classroom.id, + startTime: a.timeSlot.startTime, + endTime: a.timeSlot.endTime, + studentCount: a.studentCount + })); + + console.log('SCHEDULER OUTPUT SCHEDULE:', JSON.stringify(schedule, null, 2)); + + return { + success: true, + schedule, + message: `Successfully scheduled ${schedule.length} exams.`, + stats: { + totalCourses: courses.length, + scheduledCourses: schedule.length, + totalTimeSlots: timeSlots.length, + totalClassrooms: classrooms.length, + generationTimeMs: endTime - startTime + } + }; +} + +// ============================================================================ +// Schedule Validation (for imported/modified schedules) +// ============================================================================ + +/** + * Validates an existing schedule against all constraints. + * Useful for checking imported schedules or manual modifications. + */ +export function validateSchedule( + schedule: ExamSession[], + courses: Course[], + classrooms: Classroom[], + students: Student[] +): { valid: boolean; violations: string[] } { + const violations: string[] = []; + + // Build lookup maps + const courseMap = new Map(courses.map(c => [c.id, c])); + const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseStudentMap = buildCourseStudentMap(students); + + for (const session of schedule) { + const course = courseMap.get(session.courseId); + const classroom = classroomMap.get(session.classroomId); + + if (!course) { + violations.push(`Session ${session.id}: Course ${session.courseId} not found.`); + continue; + } + + if (!classroom) { + violations.push(`Session ${session.id}: Classroom ${session.classroomId} not found.`); + continue; + } + + // Check capacity + if (classroom.capacity < course.enrolledStudents) { + violations.push( + `Session ${session.id}: Classroom ${classroom.name} (capacity ${classroom.capacity}) ` + + `cannot fit ${course.enrolledStudents} students from ${course.code}.` + ); + } + } + + // Check for time conflicts + for (let i = 0; i < schedule.length; i++) { + for (let j = i + 1; j < schedule.length; j++) { + const s1 = schedule[i]; + const s2 = schedule[j]; + + // Check same room at same time + if (s1.classroomId === s2.classroomId) { + if (s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Room conflict: ${s1.courseId} and ${s2.courseId} overlap in classroom ${s1.classroomId}.` + ); + } + } + + // Check student conflicts + const students1 = courseStudentMap.get(s1.courseId) || []; + const students2 = courseStudentMap.get(s2.courseId) || []; + const commonStudents = students1.filter(s => students2.includes(s)); + + if (commonStudents.length > 0 && + s1.startTime < s2.endTime && s2.startTime < s1.endTime) { + violations.push( + `Student conflict: ${commonStudents.length} student(s) have overlapping exams ` + + `for ${s1.courseId} and ${s2.courseId}.` + ); + } + } + } + + return { + valid: violations.length === 0, + violations + }; +} diff --git a/.history/src/components/DataInput_20251221022845.tsx b/.history/src/components/DataInput_20251221022845.tsx new file mode 100644 index 0000000..5f3637d --- /dev/null +++ b/.history/src/components/DataInput_20251221022845.tsx @@ -0,0 +1,1261 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: Course | Classroom; + type: 'courses' | 'classrooms'; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const isCourse = type === 'courses'; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (!isCourse && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {isCourse ? t('dataInput.courses') : t('dataInput.classrooms')} +

+ +
+
+ {isCourse ? ( + <> +
+ + +
+
+ + +
+ +
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ) : ( + <> +
+ + +
+
+ + +
+
+ + +
+ + + )} + +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ + {/* Student Info Card */} +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs and names (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ + {/* Attendance Card */} +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + await window.api.clearSchedule(); + + // 2. Update frontend state + setCourses([]); + setClassrooms([]); + setStudents([]); + setSchedule([]); // Assuming this setter is for the schedule data + + showNotification("All data and generated schedules have been wiped.", 'success'); + + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if Courses or Students (in-app or temporary) are missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information before importing attendance!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'info'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + if (activeTab === 'students') return; + + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + // For single add, we might want a single add API, but for now let's use bulk or just update local state? + // The user asked to fix "Imported data", but manual add should probably also persist. + // The current plan focused on Import. Let's stick to fixing Import first, but ideally we should fix manual add too. + // However, the current `addCourse` in preload only takes `course`. + // Let's assume for now we just update state for manual add, or better, call the API if available. + // `window.api.addCourse` exists. + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + // Update not implemented in backend yet for single item update. + // Just update local state for now. + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + // No single add API for classrooms yet in my plan, but I can use bulk with 1 item. + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } + setIsModalOpen(false); + setEditingItem(null); + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: { id: string, code: string, name: string, enrolledStudents: number }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + }); + }); + + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: { name: string; capacity: number; building: string }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + building: 'Main Hall' + }); + } + }); + + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + // After bulk adding, get the complete fresh list from the DB + const allSavedRooms = await window.api.getClassrooms(); + // Map it correctly + const mappedRooms = allSavedRooms.map((r: any) => ({ + id: r.id.toString(), // The correct mapping + name: r.name, + capacity: r.capacity, + building: r.building + })); + // Replace the entire state, not just append + setClassrooms(mappedRooms); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } else { + // Maybe show error if nothing to save? + // But if they clicked Done with nothing, maybe just close. + } + setIsStudentImportModalOpen(false); + }; + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + // Refreshes local state from the database to ensure UI is up to date + const refreshSystemData = async () => { + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + }; + + // Processes the attendance file and links ONLY existing students to existing courses + // Processes attendance by matching file data against existing system data + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const rawMap = extractAttendanceMap(rows); + const validEnrollments: any[] = []; + + // Create lookup sets for O(1) performance + const existingStudentIds = new Set(students.map(s => s.id.toUpperCase())); + const existingCourseCodes = new Set(courses.map(c => c.code.toUpperCase())); + + let totalFoundInFile = 0; + let matchedCount = 0; + let missingStudentsCount = 0; + + rawMap.forEach((courseCodes, studentId) => { + const normalizedId = studentId.toUpperCase(); + totalFoundInFile++; + + // CHECK: Does this student exist in the Students tab? + if (existingStudentIds.has(normalizedId)) { + // Filter courses: only keep those that exist in the Courses tab + const validCourses = Array.from(courseCodes).filter(code => + existingCourseCodes.has(code.toUpperCase()) + ); + + if (validCourses.length > 0) { + validEnrollments.push({ + studentNumber: studentId, + name: "", // Leave empty; backend won't update existing names + enrolledCourses: validCourses + }); + matchedCount++; + } + } else { + // Student not found in system - increment skip counter + missingStudentsCount++; + } + }); + + // If we found at least one match, proceed with the import + if (validEnrollments.length > 0) { + await window.api.addStudentsBulk(validEnrollments); + + // Notify user about the partial success and the missing students + if (missingStudentsCount > 0) { + showNotification( + `Matched ${matchedCount} students. Records not found for ${missingStudentsCount} students in the list.`, + 'success' + ); + } else { + showNotification(`Successfully matched all ${matchedCount} students.`, 'success'); + } + + // Refresh UI data + await refreshSystemData(); + return matchedCount; + } else { + showNotification(`Import Failed: None of the ${totalFoundInFile} students in this file exist in your system.`, 'error'); + return 0; + } + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + if (text.includes(';') || rows[0].includes('CLASSROOMS')) { + parseClassroomFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'courses') { + if (rows[0].includes('COURSES IN THE SYSTEM') || rows.some(r => r.startsWith('CourseCode_'))) { + parseCourseListFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'students') { + if (text.includes('[') && text.includes(']')) { + parseStudentAttendanceFile(rows); + } else if (rows[0].includes('ALL OF THE STUDENTS') || rows.some(r => r.startsWith('Std_ID_'))) { + parseSimpleStudentList(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const actualType = detectFileType(text); + + // VALIDATION: Ensure the file type matches the current active tab + if (actualType !== activeTab) { + showNotification(`Tab Mismatch: You cannot upload ${actualType} file in ${activeTab} tab!`, 'error'); + return; + } + + try { + processCSV(text); + } catch (err) { + showNotification("Import Error: Failed to process the CSV file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)} + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + + {c} + + ))} + {s.enrolledCourses.length > 3 && ( + +{s.enrolledCourses.length - 3} more + )} +
+ ) : ( + None + )} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && (activeTab === 'courses' || activeTab === 'classrooms') && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221052757.tsx b/.history/src/components/DataInput_20251221052757.tsx new file mode 100644 index 0000000..64887f5 --- /dev/null +++ b/.history/src/components/DataInput_20251221052757.tsx @@ -0,0 +1,1261 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: Course | Classroom; + type: 'courses' | 'classrooms'; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const isCourse = type === 'courses'; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (!isCourse && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {isCourse ? t('dataInput.courses') : t('dataInput.classrooms')} +

+ +
+
+ {isCourse ? ( + <> +
+ + +
+
+ + +
+ +
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ) : ( + <> +
+ + +
+
+ + +
+
+ + +
+ + + )} + +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ + {/* Student Info Card */} +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs and names (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ + {/* Attendance Card */} +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + await window.api.clearSchedule(); + + // 2. Update frontend state + setCourses([]); + setClassrooms([]); + setStudents([]); + // Assuming this setter is for the schedule data + + showNotification("All data and generated schedules have been wiped.", 'success'); + + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if Courses or Students (in-app or temporary) are missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information before importing attendance!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + if (activeTab === 'students') return; + + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + // For single add, we might want a single add API, but for now let's use bulk or just update local state? + // The user asked to fix "Imported data", but manual add should probably also persist. + // The current plan focused on Import. Let's stick to fixing Import first, but ideally we should fix manual add too. + // However, the current `addCourse` in preload only takes `course`. + // Let's assume for now we just update state for manual add, or better, call the API if available. + // `window.api.addCourse` exists. + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + // Update not implemented in backend yet for single item update. + // Just update local state for now. + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + // No single add API for classrooms yet in my plan, but I can use bulk with 1 item. + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } + setIsModalOpen(false); + setEditingItem(null); + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: { id: string, code: string, name: string, enrolledStudents: number }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + }); + }); + + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: { name: string; capacity: number; building: string }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + building: 'Main Hall' + }); + } + }); + + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + // After bulk adding, get the complete fresh list from the DB + const allSavedRooms = await window.api.getClassrooms(); + // Map it correctly + const mappedRooms = allSavedRooms.map((r: any) => ({ + id: r.id.toString(), // The correct mapping + name: r.name, + capacity: r.capacity, + building: r.building + })); + // Replace the entire state, not just append + setClassrooms(mappedRooms); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } else { + // Maybe show error if nothing to save? + // But if they clicked Done with nothing, maybe just close. + } + setIsStudentImportModalOpen(false); + }; + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + // Refreshes local state from the database to ensure UI is up to date + const refreshSystemData = async () => { + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + }; + + // Processes the attendance file and links ONLY existing students to existing courses + // Processes attendance by matching file data against existing system data + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const rawMap = extractAttendanceMap(rows); + const validEnrollments: any[] = []; + + // Create lookup sets for O(1) performance + const existingStudentIds = new Set(students.map(s => s.id.toUpperCase())); + const existingCourseCodes = new Set(courses.map(c => c.code.toUpperCase())); + + let totalFoundInFile = 0; + let matchedCount = 0; + let missingStudentsCount = 0; + + rawMap.forEach((courseCodes, studentId) => { + const normalizedId = studentId.toUpperCase(); + totalFoundInFile++; + + // CHECK: Does this student exist in the Students tab? + if (existingStudentIds.has(normalizedId)) { + // Filter courses: only keep those that exist in the Courses tab + const validCourses = Array.from(courseCodes).filter(code => + existingCourseCodes.has(code.toUpperCase()) + ); + + if (validCourses.length > 0) { + validEnrollments.push({ + studentNumber: studentId, + name: "", // Leave empty; backend won't update existing names + enrolledCourses: validCourses + }); + matchedCount++; + } + } else { + // Student not found in system - increment skip counter + missingStudentsCount++; + } + }); + + // If we found at least one match, proceed with the import + if (validEnrollments.length > 0) { + await window.api.addStudentsBulk(validEnrollments); + + // Notify user about the partial success and the missing students + if (missingStudentsCount > 0) { + showNotification( + `Matched ${matchedCount} students. Records not found for ${missingStudentsCount} students in the list.`, + 'success' + ); + } else { + showNotification(`Successfully matched all ${matchedCount} students.`, 'success'); + } + + // Refresh UI data + await refreshSystemData(); + return matchedCount; + } else { + showNotification(`Import Failed: None of the ${totalFoundInFile} students in this file exist in your system.`, 'error'); + return 0; + } + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + if (text.includes(';') || rows[0].includes('CLASSROOMS')) { + parseClassroomFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'courses') { + if (rows[0].includes('COURSES IN THE SYSTEM') || rows.some(r => r.startsWith('CourseCode_'))) { + parseCourseListFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'students') { + if (text.includes('[') && text.includes(']')) { + parseStudentAttendanceFile(rows); + } else if (rows[0].includes('ALL OF THE STUDENTS') || rows.some(r => r.startsWith('Std_ID_'))) { + parseSimpleStudentList(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const actualType = detectFileType(text); + + // VALIDATION: Ensure the file type matches the current active tab + if (actualType !== activeTab) { + showNotification(`Tab Mismatch: You cannot upload ${actualType} file in ${activeTab} tab!`, 'error'); + return; + } + + try { + processCSV(text); + } catch (err) { + showNotification("Import Error: Failed to process the CSV file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)} + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + + {c} + + ))} + {s.enrolledCourses.length > 3 && ( + +{s.enrolledCourses.length - 3} more + )} +
+ ) : ( + None + )} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && (activeTab === 'courses' || activeTab === 'classrooms') && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221175532.tsx b/.history/src/components/DataInput_20251221175532.tsx new file mode 100644 index 0000000..f9febe1 --- /dev/null +++ b/.history/src/components/DataInput_20251221175532.tsx @@ -0,0 +1,973 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: Course | Classroom | Student; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: Course | Classroom | Student) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + + + + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + setCourses([]); setClassrooms([]); setStudents([]); + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Helper function to detect the type of the imported file based on its content + const detectFileType = (text: string): string => { + const cleanText = text.trim(); + if (cleanText.includes('ALL OF THE COURSES')) return 'courses'; + if (cleanText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + if (cleanText.includes('ALL OF THE STUDENTS')) return 'students'; + // Check for the specific Python-like list format: [ 'STD001', ... ] + if (cleanText.includes('[') && cleanText.includes(']')) return 'enrollments'; + return 'unknown'; + }; + + const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information first!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check if the file content matches the Enrollment format + const detected = detectFileType(text); + if (detected !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + map.forEach((val, key) => { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + }); + setAttendanceStatus({ fileName: file.name, count: map.size }); + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom | Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: Course[] = []; + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); + newCourses.push({ id: code, code: code, name: `Course ${code}`, enrolledStudents: 0 }); + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // Refresh from DB to be sure or just update state + const savedCourses = await window.api.getCourses(); + // Map DB result to frontend type if needed, but they are similar. + // DB has `enrolled_students`, frontend `enrolledStudents`. + // We need to map it. + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, // Use code as ID for frontend consistency or c.id? Frontend uses string ID often. + // The current frontend uses `code` as `id` for courses often. + // Let's check `Course` type. + // In `types/index.ts` (not seen but inferred), `id` is likely string or number. + // In `DataInput.tsx` line 211: `id` is `code`. + // So let's keep using code as ID for now or map properly. + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + + setCourses(prev => { + const existingIds = new Set(prev.map(c => c.id)); + const uniqueNew = mappedCourses.filter((c: any) => !existingIds.has(c.id)); + return [...prev, ...uniqueNew]; + }); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: Classroom[] = []; + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + newRooms.push({ id: name, name: name, capacity: isNaN(capacity) ? 0 : capacity, building: 'Main Hall' }); + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + const saved = await window.api.getClassrooms(); + setClassrooms(saved.map((r: any) => ({ id: r.name, name: r.name, capacity: r.capacity, building: r.building }))); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Buffer Mode + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + const count = map.size; // Showing map.size is count from this file + setStudentInfoStatus({ fileName: file.name, count }); + } catch (err) { + console.error("Failed to parse student info file", err); + showNotification(t('dataInput.parseError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221182500.tsx b/.history/src/components/DataInput_20251221182500.tsx new file mode 100644 index 0000000..64887f5 --- /dev/null +++ b/.history/src/components/DataInput_20251221182500.tsx @@ -0,0 +1,1261 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: Course | Classroom; + type: 'courses' | 'classrooms'; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const isCourse = type === 'courses'; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (!isCourse && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {isCourse ? t('dataInput.courses') : t('dataInput.classrooms')} +

+ +
+
+ {isCourse ? ( + <> +
+ + +
+
+ + +
+ +
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ) : ( + <> +
+ + +
+
+ + +
+
+ + +
+ + + )} + +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ + {/* Student Info Card */} +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs and names (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ + {/* Attendance Card */} +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + await window.api.clearSchedule(); + + // 2. Update frontend state + setCourses([]); + setClassrooms([]); + setStudents([]); + // Assuming this setter is for the schedule data + + showNotification("All data and generated schedules have been wiped.", 'success'); + + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if Courses or Students (in-app or temporary) are missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information before importing attendance!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + if (activeTab === 'students') return; + + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + // For single add, we might want a single add API, but for now let's use bulk or just update local state? + // The user asked to fix "Imported data", but manual add should probably also persist. + // The current plan focused on Import. Let's stick to fixing Import first, but ideally we should fix manual add too. + // However, the current `addCourse` in preload only takes `course`. + // Let's assume for now we just update state for manual add, or better, call the API if available. + // `window.api.addCourse` exists. + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + // Update not implemented in backend yet for single item update. + // Just update local state for now. + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + // No single add API for classrooms yet in my plan, but I can use bulk with 1 item. + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } + setIsModalOpen(false); + setEditingItem(null); + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: { id: string, code: string, name: string, enrolledStudents: number }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + }); + }); + + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: { name: string; capacity: number; building: string }[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + building: 'Main Hall' + }); + } + }); + + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + // After bulk adding, get the complete fresh list from the DB + const allSavedRooms = await window.api.getClassrooms(); + // Map it correctly + const mappedRooms = allSavedRooms.map((r: any) => ({ + id: r.id.toString(), // The correct mapping + name: r.name, + capacity: r.capacity, + building: r.building + })); + // Replace the entire state, not just append + setClassrooms(mappedRooms); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } else { + // Maybe show error if nothing to save? + // But if they clicked Done with nothing, maybe just close. + } + setIsStudentImportModalOpen(false); + }; + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + // Refreshes local state from the database to ensure UI is up to date + const refreshSystemData = async () => { + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + }; + + // Processes the attendance file and links ONLY existing students to existing courses + // Processes attendance by matching file data against existing system data + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const rawMap = extractAttendanceMap(rows); + const validEnrollments: any[] = []; + + // Create lookup sets for O(1) performance + const existingStudentIds = new Set(students.map(s => s.id.toUpperCase())); + const existingCourseCodes = new Set(courses.map(c => c.code.toUpperCase())); + + let totalFoundInFile = 0; + let matchedCount = 0; + let missingStudentsCount = 0; + + rawMap.forEach((courseCodes, studentId) => { + const normalizedId = studentId.toUpperCase(); + totalFoundInFile++; + + // CHECK: Does this student exist in the Students tab? + if (existingStudentIds.has(normalizedId)) { + // Filter courses: only keep those that exist in the Courses tab + const validCourses = Array.from(courseCodes).filter(code => + existingCourseCodes.has(code.toUpperCase()) + ); + + if (validCourses.length > 0) { + validEnrollments.push({ + studentNumber: studentId, + name: "", // Leave empty; backend won't update existing names + enrolledCourses: validCourses + }); + matchedCount++; + } + } else { + // Student not found in system - increment skip counter + missingStudentsCount++; + } + }); + + // If we found at least one match, proceed with the import + if (validEnrollments.length > 0) { + await window.api.addStudentsBulk(validEnrollments); + + // Notify user about the partial success and the missing students + if (missingStudentsCount > 0) { + showNotification( + `Matched ${matchedCount} students. Records not found for ${missingStudentsCount} students in the list.`, + 'success' + ); + } else { + showNotification(`Successfully matched all ${matchedCount} students.`, 'success'); + } + + // Refresh UI data + await refreshSystemData(); + return matchedCount; + } else { + showNotification(`Import Failed: None of the ${totalFoundInFile} students in this file exist in your system.`, 'error'); + return 0; + } + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + if (text.includes(';') || rows[0].includes('CLASSROOMS')) { + parseClassroomFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'courses') { + if (rows[0].includes('COURSES IN THE SYSTEM') || rows.some(r => r.startsWith('CourseCode_'))) { + parseCourseListFile(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } else if (activeTab === 'students') { + if (text.includes('[') && text.includes(']')) { + parseStudentAttendanceFile(rows); + } else if (rows[0].includes('ALL OF THE STUDENTS') || rows.some(r => r.startsWith('Std_ID_'))) { + parseSimpleStudentList(rows); + } else { + showNotification(t('dataInput.formatError'), 'error'); + } + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const actualType = detectFileType(text); + + // VALIDATION: Ensure the file type matches the current active tab + if (actualType !== activeTab) { + showNotification(`Tab Mismatch: You cannot upload ${actualType} file in ${activeTab} tab!`, 'error'); + return; + } + + try { + processCSV(text); + } catch (err) { + showNotification("Import Error: Failed to process the CSV file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)} + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + + {c} + + ))} + {s.enrolledCourses.length > 3 && ( + +{s.enrolledCourses.length - 3} more + )} +
+ ) : ( + None + )} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && (activeTab === 'courses' || activeTab === 'classrooms') && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221182844.tsx b/.history/src/components/DataInput_20251221182844.tsx new file mode 100644 index 0000000..4605554 --- /dev/null +++ b/.history/src/components/DataInput_20251221182844.tsx @@ -0,0 +1,1314 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: any; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); +<<<<<<< HEAD + await window.api.clearSchedule(); + + // 2. Update frontend state + setCourses([]); + setClassrooms([]); + setStudents([]); + // Assuming this setter is for the schedule data + + showNotification("All data and generated schedules have been wiped.", 'success'); + + + setCourses([]); setClassrooms([]); setStudents([]); +>>>>>>> origin/main + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + +<<<<<<< HEAD + // CONSTRAINT: Prevent import if Courses or Students (in-app or temporary) are missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information before importing attendance!", 'error'); +======= + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information first!", 'error'); +>>>>>>> origin/main + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom| Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { +<<<<<<< HEAD + const newCourses: { id: string, code: string, name: string, enrolledStudents: number }[] = []; + +======= + const newCourses: Course[] = []; +>>>>>>> origin/main + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); +<<<<<<< HEAD + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + }); +======= + newCourses.push({ id: code, code: code, name: `Course ${code}`, enrolledStudents: 0 }); +>>>>>>> origin/main + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { +<<<<<<< HEAD + const newRooms: { name: string; capacity: number; building: string }[] = []; + +======= + const newRooms: Classroom[] = []; +>>>>>>> origin/main + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); +<<<<<<< HEAD + + newRooms.push({ + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + building: 'Main Hall' + }); +======= + newRooms.push({ id: name, name: name, capacity: isNaN(capacity) ? 0 : capacity, building: 'Main Hall' }); +>>>>>>> origin/main + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); +<<<<<<< HEAD + // After bulk adding, get the complete fresh list from the DB + const allSavedRooms = await window.api.getClassrooms(); + // Map it correctly + const mappedRooms = allSavedRooms.map((r: any) => ({ + id: r.id.toString(), // The correct mapping + name: r.name, + capacity: r.capacity, + building: r.building + })); + // Replace the entire state, not just append + setClassrooms(mappedRooms); +======= + const saved = await window.api.getClassrooms(); + setClassrooms(saved.map((r: any) => ({ id: r.name, name: r.name, capacity: r.capacity, building: r.building }))); +>>>>>>> origin/main + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + +<<<<<<< HEAD + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + // Refreshes local state from the database to ensure UI is up to date + const refreshSystemData = async () => { + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + }; + + // Processes the attendance file and links ONLY existing students to existing courses + // Processes attendance by matching file data against existing system data + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const rawMap = extractAttendanceMap(rows); + const validEnrollments: any[] = []; + + // Create lookup sets for O(1) performance + const existingStudentIds = new Set(students.map(s => s.id.toUpperCase())); + const existingCourseCodes = new Set(courses.map(c => c.code.toUpperCase())); + + let totalFoundInFile = 0; + let matchedCount = 0; + let missingStudentsCount = 0; + + rawMap.forEach((courseCodes, studentId) => { + const normalizedId = studentId.toUpperCase(); + totalFoundInFile++; + + // CHECK: Does this student exist in the Students tab? + if (existingStudentIds.has(normalizedId)) { + // Filter courses: only keep those that exist in the Courses tab + const validCourses = Array.from(courseCodes).filter(code => + existingCourseCodes.has(code.toUpperCase()) + ); + + if (validCourses.length > 0) { + validEnrollments.push({ + studentNumber: studentId, + name: "", // Leave empty; backend won't update existing names + enrolledCourses: validCourses + }); + matchedCount++; + } + } else { + // Student not found in system - increment skip counter + missingStudentsCount++; + } + }); + + // If we found at least one match, proceed with the import + if (validEnrollments.length > 0) { + await window.api.addStudentsBulk(validEnrollments); + + // Notify user about the partial success and the missing students + if (missingStudentsCount > 0) { + showNotification( + `Matched ${matchedCount} students. Records not found for ${missingStudentsCount} students in the list.`, + 'success' + ); + } else { + showNotification(`Successfully matched all ${matchedCount} students.`, 'success'); + } + + // Refresh UI data + await refreshSystemData(); + return matchedCount; + } else { + showNotification(`Import Failed: None of the ${totalFoundInFile} students in this file exist in your system.`, 'error'); + return 0; + } + }; + +======= + +>>>>>>> origin/main + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221183058.tsx b/.history/src/components/DataInput_20251221183058.tsx new file mode 100644 index 0000000..e716c1b --- /dev/null +++ b/.history/src/components/DataInput_20251221183058.tsx @@ -0,0 +1,1230 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: any; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + setCourses([]); + setClassrooms([]); + setStudents([]); + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + if (courses.length === 0 || students.length === 0) { + showNotification("Dependency Error: Please import Courses and Students first!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom| Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: Course[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + + }); + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: Classroom[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + id: name, + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + + building: 'Main Hall' + }); + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + const savedRooms = await window.api.getClassrooms(); + const mappedRooms = savedRooms.map((r: any) => ({ + id: r.name, + name: r.name, + capacity: r.capacity, + building: r.building + })); + + setClassrooms(prev => { + const existingIds = new Set(prev.map(r => r.id)); + const uniqueNew = mappedRooms.filter((r: any) => !existingIds.has(r.id)); + return [...prev, ...uniqueNew]; + }); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const map = extractAttendanceMap(rows); + const newStudents: any[] = []; + map.forEach((courses, id) => { + newStudents.push({ + studentNumber: id, + name: `Student ${id}`, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + showNotification(t('dataInput.processedAttendance', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221193006.tsx b/.history/src/components/DataInput_20251221193006.tsx new file mode 100644 index 0000000..e716c1b --- /dev/null +++ b/.history/src/components/DataInput_20251221193006.tsx @@ -0,0 +1,1230 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: any; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + setCourses([]); + setClassrooms([]); + setStudents([]); + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + if (courses.length === 0 || students.length === 0) { + showNotification("Dependency Error: Please import Courses and Students first!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom| Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: Course[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + + }); + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: Classroom[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + id: name, + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + + building: 'Main Hall' + }); + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + const savedRooms = await window.api.getClassrooms(); + const mappedRooms = savedRooms.map((r: any) => ({ + id: r.name, + name: r.name, + capacity: r.capacity, + building: r.building + })); + + setClassrooms(prev => { + const existingIds = new Set(prev.map(r => r.id)); + const uniqueNew = mappedRooms.filter((r: any) => !existingIds.has(r.id)); + return [...prev, ...uniqueNew]; + }); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const map = extractAttendanceMap(rows); + const newStudents: any[] = []; + map.forEach((courses, id) => { + newStudents.push({ + studentNumber: id, + name: `Student ${id}`, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + showNotification(t('dataInput.processedAttendance', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221193655.tsx b/.history/src/components/DataInput_20251221193655.tsx new file mode 100644 index 0000000..e49b0af --- /dev/null +++ b/.history/src/components/DataInput_20251221193655.tsx @@ -0,0 +1,1093 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: any; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + setCourses([]); setClassrooms([]); setStudents([]); + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Helper function to detect the type of the imported file based on its content + const detectFileType = (text: string): string => { + const cleanText = text.trim(); + if (cleanText.includes('ALL OF THE COURSES')) return 'courses'; + if (cleanText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + if (cleanText.includes('ALL OF THE STUDENTS')) return 'students'; + // Check for the specific Python-like list format: [ 'STD001', ... ] + if (cleanText.includes('[') && cleanText.includes(']')) return 'enrollments'; + return 'unknown'; + }; + + const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; + if (courses.length === 0 || !studentsAvailable) { + showNotification("Dependency Error: Please import Courses and student information first!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check if the file content matches the Enrollment format + const detected = detectFileType(text); + if (detected !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + map.forEach((val, key) => { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + }); + setAttendanceStatus({ fileName: file.name, count: map.size }); + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom | Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: Course[] = []; + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); + newCourses.push({ id: code, code: code, name: `Course ${code}`, enrolledStudents: 0 }); + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // Refresh from DB to be sure or just update state + const savedCourses = await window.api.getCourses(); + // Map DB result to frontend type if needed, but they are similar. + // DB has `enrolled_students`, frontend `enrolledStudents`. + // We need to map it. + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, // Use code as ID for frontend consistency or c.id? Frontend uses string ID often. + // The current frontend uses `code` as `id` for courses often. + // Let's check `Course` type. + // In `types/index.ts` (not seen but inferred), `id` is likely string or number. + // In `DataInput.tsx` line 211: `id` is `code`. + // So let's keep using code as ID for now or map properly. + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + + setCourses(prev => { + const existingIds = new Set(prev.map(c => c.id)); + const uniqueNew = mappedCourses.filter((c: any) => !existingIds.has(c.id)); + return [...prev, ...uniqueNew]; + }); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: Classroom[] = []; + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + newRooms.push({ id: name, name: name, capacity: isNaN(capacity) ? 0 : capacity, building: 'Main Hall' }); + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + const saved = await window.api.getClassrooms(); + setClassrooms(saved.map((r: any) => ({ id: r.name, name: r.name, capacity: r.capacity, building: r.building }))); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Buffer Mode + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + const count = map.size; // Showing map.size is count from this file + setStudentInfoStatus({ fileName: file.name, count }); + } catch (err) { + console.error("Failed to parse student info file", err); + showNotification(t('dataInput.parseError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/DataInput_20251221194331.tsx b/.history/src/components/DataInput_20251221194331.tsx new file mode 100644 index 0000000..e69de29 diff --git a/.history/src/components/DataInput_20251221194753.tsx b/.history/src/components/DataInput_20251221194753.tsx new file mode 100644 index 0000000..e716c1b --- /dev/null +++ b/.history/src/components/DataInput_20251221194753.tsx @@ -0,0 +1,1230 @@ +import React, { useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNotification } from '../context/NotificationContext'; +import { Course, Classroom, Student } from '../types'; + +interface DataInputProps { + courses: Course[]; + setCourses: React.Dispatch>; + classrooms: Classroom[]; + setClassrooms: React.Dispatch>; + students: Student[]; + setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; +} + +type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; + +interface EditModalProps { + item: any; + type: Tab; + mode: 'add' | 'edit'; + onClose: () => void; + onSave: (updatedItem: any) => void; +} + +const EditModal: React.FC = ({ item, type, mode, onClose, onSave }) => { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ ...item }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type: inputType } = e.target; + + if (name === 'capacity') { + if (value === '') { + setFormData((prev: any) => ({ ...prev, [name]: '' })); + } else if (/^\d+$/.test(value)) { + setFormData((prev: any) => ({ ...prev, [name]: parseInt(value, 10) })); + } + return; + } + + setFormData((prev: any) => ({ + ...prev, + [name]: inputType === 'number' ? parseInt(value, 10) : value + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const finalData = { ...formData }; + if (type === 'classrooms' && (typeof finalData.capacity !== 'number' || isNaN(finalData.capacity))) { + finalData.capacity = 0; + } + onSave(finalData); + }; + + const renderFormContent = () => { + switch (type) { + case 'courses': + return ( + <> +
+ + +
+
+ + +
+
+ + +

{t('dataInput.calculatedAutomatically')}

+
+ + ); + case 'classrooms': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + case 'students': + return ( + <> +
+ + +
+
+ + +
+
+ + +
+ + ); + default: + return null; + } + }; + + return ( +
+
+
+

+ {mode === 'add' ? t('dataInput.addNew') : t('common.edit')} {t(`dataInput.${type}`)} +

+ +
+
+ {renderFormContent()} +
+ + +
+
+
+
+ ); +}; + +interface ImportStatus { + fileName: string; + count: number; +} + +interface StudentImportModalProps { + onClose: () => void; + onSelectStudentInfo: () => void; + onSelectAttendance: () => void; + studentInfoStatus: ImportStatus | null; + attendanceStatus: ImportStatus | null; + onSave: () => void; +} + +const StudentImportModal: React.FC = ({ + onClose, + onSelectStudentInfo, + onSelectAttendance, + studentInfoStatus, + attendanceStatus, + onSave +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+

+ {t('dataInput.importStudentsTitle') || 'Import Student Data'} +

+ +
+
+

+ {t('dataInput.importStudentsDesc') || 'Select the type of file you want to import:'} +

+ +
+
+
+ + + + + + +
+
+

+ {t('dataInput.importStudentInfo') || 'Student Information'} +

+

+ {studentInfoStatus + ? {studentInfoStatus.fileName}
{studentInfoStatus.count} records found
+ : (t('dataInput.importStudentInfoDesc') || 'Import a file containing student IDs (e.g., realData_AllStudents.csv)') + } +

+
+
+ {studentInfoStatus ? ( + + ) : ( + + )} +
+
+
+ +
+
+
+ + + + + + + +
+
+

+ {t('dataInput.importAttendance') || 'Attendance Data'} +

+

+ {attendanceStatus + ? {attendanceStatus.fileName}
{attendanceStatus.count} records found
+ : (t('dataInput.importAttendanceDesc') || 'Import a file containing course-student enrollments (e.g., realData_AllAttendanceLists.csv)') + } +

+
+
+ {attendanceStatus ? ( + + ) : ( + + )} +
+
+
+
+
+ +
+
+
+ ); +}; + + + +export const DataInput: React.FC = ({ + courses, setCourses, classrooms, setClassrooms, students, setStudents +}) => { + const { t } = useTranslation(); + const { showNotification } = useNotification(); + const [activeTab, setActiveTab] = useState('courses'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' } | null>(null); + const fileInputRef = useRef(null); + + + const studentInfoFileInputRef = useRef(null); + const attendanceFileInputRef = useRef(null); + + const [editingItem, setEditingItem] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalMode, setModalMode] = useState<'add' | 'edit'>('edit'); + const [isClearMenuOpen, setIsClearMenuOpen] = useState(false); + const [isStudentImportModalOpen, setIsStudentImportModalOpen] = useState(false); + + // Status state for the modal + const [studentInfoStatus, setStudentInfoStatus] = useState(null); + const [attendanceStatus, setAttendanceStatus] = useState(null); + + // Buffers for storing data during modal session + const tempStudentMap = useRef>(new Map()); // ID -> Name + const tempAttendanceMap = useRef>>(new Map()); // ID -> Set + + const handleClearData = async (type: 'all' | 'courses' | 'classrooms' | 'students') => { + const confirmMessage = type === 'all' + ? t('dataInput.confirmDeleteAll') + : t('dataInput.confirmDeleteSection', { section: type }); + + if (window.confirm(confirmMessage)) { + if (type === 'all') { + // 1. Clear input data from DB + await window.api.clearCourses(); + await window.api.clearClassrooms(); + await window.api.clearStudents(); + setCourses([]); + setClassrooms([]); + setStudents([]); + } else if (type === 'courses') { + await window.api.clearCourses(); + setCourses([]); + } else if (type === 'classrooms') { + await window.api.clearClassrooms(); + setClassrooms([]); + } else if (type === 'students') { + await window.api.clearStudents(); + setStudents([]); + } + setIsClearMenuOpen(false); + } + }; + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + + const detectFileType = (text: string): string => { + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + + return 'unknown'; + }; + + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // CONSTRAINT: Prevent import if primary data (courses/students) is missing + if (courses.length === 0 || students.length === 0) { + showNotification("Dependency Error: Please import Courses and Students first!", 'error'); + e.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { + showNotification("Format Error: This file is not a valid attendance list file!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + + map.forEach((val, key) => { + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } + }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } + } catch (err) { + showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + const handleImportClick = () => { + if (activeTab === 'students') { + // Reset status and buffers when opening + setStudentInfoStatus(null); + setAttendanceStatus(null); + tempStudentMap.current.clear(); + tempAttendanceMap.current.clear(); + setIsStudentImportModalOpen(true); + } else { + fileInputRef.current?.click(); + } + }; + + + const handleStudentInfoImport = () => { + // Do not close modal here anymore + studentInfoFileInputRef.current?.click(); + }; + + const handleAttendanceImport = () => { + // Do not close modal here anymore + attendanceFileInputRef.current?.click(); + }; + + + const handleEditClick = (item: Course | Classroom| Student) => { + setEditingItem(item); + setModalMode('edit'); + setIsModalOpen(true); + }; + + const handleAddClick = () => { + setModalMode('add'); + if (activeTab === 'courses') { + setEditingItem({ id: '', code: '', name: '', enrolledStudents: 0 }); + } else if (activeTab === 'classrooms') { + setEditingItem({ id: '', name: '', capacity: 0, building: '' }); + } else if (activeTab === 'students') { + setEditingItem({ id: '', name: '', email: '', enrolledCourses: [] }); + } + setIsModalOpen(true); + }; + + const handleSaveItem = async (updatedItem: any) => { + try { + if (activeTab === 'courses') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.code }; + await window.api.addCourse(newItem); + setCourses(prev => [...prev, newItem]); + } else { + setCourses(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'classrooms') { + if (modalMode === 'add') { + const newItem = { ...updatedItem, id: updatedItem.name }; + await window.api.addClassroomsBulk([newItem]); + setClassrooms(prev => [...prev, newItem]); + } else { + setClassrooms(prev => prev.map(c => c.id === updatedItem.id ? updatedItem : c)); + } + } else if (activeTab === 'students') { + if (modalMode === 'add') { + const newItem = { + ...updatedItem, + email: updatedItem.email || `${updatedItem.id.toLowerCase()}@uni.edu`, + enrolledCourses: [] + }; + await window.api.addStudentsBulk([ + { + studentNumber: newItem.id, + name: newItem.name, + enrolledCourses: [] + } + ]); + setStudents(prev => [...prev, newItem]); + showNotification(t('dataInput.studentAdded', 'Student added successfully'), 'success'); + } else { + setStudents(prev => prev.map(s => s.id === updatedItem.id ? updatedItem : s)); + } + } + setIsModalOpen(false); + setEditingItem(null); + } catch (error) { + console.error("Save error:", error); + showNotification("Failed to save item", 'error'); + } + }; + + + const parseCourseListFile = async (rows: string[]) => { + const newCourses: Course[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE COURSES") || !row.trim()) return; + const code = row.trim(); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + + }); + }); + if (newCourses.length > 0) { + await window.api.addCoursesBulk(newCourses); + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + // Replace the entire state + setCourses(mappedCourses); + showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); + } + }; + + const parseClassroomFile = async (rows: string[]) => { + const newRooms: Classroom[] = []; + + rows.forEach((row) => { + if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; + const parts = row.split(';'); + if (parts.length >= 2) { + const name = parts[0].trim(); + const capacity = parseInt(parts[1].trim(), 10); + + newRooms.push({ + id: name, + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + + building: 'Main Hall' + }); + } + }); + if (newRooms.length > 0) { + await window.api.addClassroomsBulk(newRooms); + const savedRooms = await window.api.getClassrooms(); + const mappedRooms = savedRooms.map((r: any) => ({ + id: r.name, + name: r.name, + capacity: r.capacity, + building: r.building + })); + + setClassrooms(prev => { + const existingIds = new Set(prev.map(r => r.id)); + const uniqueNew = mappedRooms.filter((r: any) => !existingIds.has(r.id)); + return [...prev, ...uniqueNew]; + }); + showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); + } + }; + + + // --- Pure Parsing Logic --- + + const extractStudentMap = (rows: string[]): Map => { + const map = new Map(); + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow || cleanRow.includes("ALL OF THE STUDENTS")) return; + // Assuming format: ID;Name + // The previous logic was: const [student_id, student_name] = row.split(';'); + // But parseSimpleStudentList used just cleanRow as ID and generated name. + // Let's check parseSimpleStudentList again. + // It did: studentId = cleanRow; name = Student {studentId} + // Wait, the original `parseSimpleStudentList` I saw in Step 29/30 had split(';') logic commented out or replaced? + // In Step 29, line 316: `const studentId = cleanRow;` + // In Step 18 (summary): `realData_AllStudents.csv` format `student_id;student_name`. + // The code in Step 29 seemed to simplify it. + // Let's try to handle both if possible, or stick to what the code was doing. + // The code at line 424 (viewed in Step 138) was: `const studentId = cleanRow;` + // And name: `Student ${studentId}` + // BUT the user says "Student Information" file. + // Let's look at `realData_AllStudents.csv` content from Step 18 summary: `student_id;student_name` + // If the code was just taking the whole row as ID, that might be a bug or I misread. + // Let's try to split by semicolon if present. + if (cleanRow.includes(';')) { + const [id, name] = cleanRow.split(';'); + if (id) map.set(id.trim(), name?.trim() || `Student ${id.trim()}`); + } else { + map.set(cleanRow, `Student ${cleanRow}`); + } + }); + return map; + }; + + const extractAttendanceMap = (rows: string[]): Map> => { + const map = new Map>(); + let currentCourseCode = ''; + rows.forEach(row => { + const cleanRow = row.trim(); + if (!cleanRow) return; + if (cleanRow.startsWith('[')) { + if (!currentCourseCode) return; + const content = cleanRow.slice(1, -1); + // The regex replace might be needed for quoted strings + const studentIds = content.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); + studentIds.forEach(sId => { + if (!sId) return; + if (!map.has(sId)) map.set(sId, new Set()); + map.get(sId)?.add(currentCourseCode); + }); + } else { + // Heuristic for course code vs empty line + if (cleanRow.length < 50 && !cleanRow.startsWith('[')) currentCourseCode = cleanRow; + } + }); + return map; + }; + + const saveMergedData = async () => { + // Merge buffers + const allStudentIds = new Set([...tempStudentMap.current.keys(), ...tempAttendanceMap.current.keys()]); + const newStudents: any[] = []; + + allStudentIds.forEach(id => { + const name = tempStudentMap.current.get(id) || `Student ${id.split('_')[2] || id}`; + const courses = tempAttendanceMap.current.get(id) || new Set(); + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + + // Refresh data + const savedStudents = await window.api.getStudents(); + const mappedStudents = savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + })); + setStudents(mappedStudents); + + // Refresh courses for enrolled counts + const savedCourses = await window.api.getCourses(); + const mappedCourses = savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + })); + setCourses(mappedCourses); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + } + setIsStudentImportModalOpen(false); + }; + + + + // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const map = extractAttendanceMap(rows); + const newStudents: any[] = []; + map.forEach((courses, id) => { + newStudents.push({ + studentNumber: id, + name: `Student ${id}`, + enrolledCourses: Array.from(courses) + }); + }); + + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + + showNotification(t('dataInput.processedAttendance', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + + const processCSV = (text: string) => { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + if (rows.length === 0) return; + + if (activeTab === 'classrooms') { + parseClassroomFile(rows); + } else if (activeTab === 'courses') { + parseCourseListFile(rows); + } else { + // This case should ideally not be reached due to handleFileChange's detectFileType and activeTab check, + // but as a safeguard, we can notify if an unexpected tab is encountered. + showNotification(t('dataInput.formatError'), 'error'); + } + }; + +const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const text = event.target?.result as string; + const detectedType = detectFileType(text); + + if (detectedType === 'unknown') { + showNotification(t('dataInput.unrecognizedFileFormat'), 'error'); + e.target.value = ''; + return; + } + + if (detectedType !== activeTab) { + showNotification(t('dataInput.tabMismatchError', { detected: detectedType, active: activeTab }), 'error'); + e.target.value = ''; + return; + } + + try { + processCSV(text); + showNotification(t('dataInput.fileProcessed', { type: activeTab }), 'success'); + } catch (err) { + console.error("Error processing CSV:", err); + showNotification(t('dataInput.importError'), 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; + }; + + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students + const handleStudentInfoFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + + try { + const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); + // Extract students and store them in the temporary map + const map = extractStudentMap(rows); + map.forEach((val, key) => tempStudentMap.current.set(key, val)); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); + } catch (err) { + showNotification("Parse Error: Failed to process the student info file.", 'error'); + } + }; + reader.readAsText(file); + e.target.value = ''; // Reset to allow re-selection + }; + + + + // Clear search and sort when switching tabs + React.useEffect(() => { + setSearchQuery(''); + setSortConfig(null); + }, [activeTab]); + + // Toggle sort for a column + const toggleSort = (column: string) => { + setSortConfig(prev => { + if (prev?.column === column) { + // Toggle direction or clear if already desc + if (prev.direction === 'asc') { + return { column, direction: 'desc' }; + } + return null; // Clear sort + } + return { column, direction: 'asc' }; + }); + }; + + // Sort icon component + const SortIcon = ({ column }: { column: string }) => { + const isActive = sortConfig?.column === column; + const isAsc = isActive && sortConfig?.direction === 'asc'; + const isDesc = isActive && sortConfig?.direction === 'desc'; + + return ( + + + + + + + + + ); + }; + + // Get filtered and sorted data + const getFilteredData = () => { + const query = searchQuery.toLowerCase().trim(); + + // Filter + let filteredCourses = query + ? courses.filter(c => + c.code.toLowerCase().includes(query) || + c.name.toLowerCase().includes(query) + ) + : [...courses]; + + let filteredClassrooms = query + ? classrooms.filter(r => + r.name.toLowerCase().includes(query) || + r.building.toLowerCase().includes(query) + ) + : [...classrooms]; + + let filteredStudents = query + ? students.filter(s => + s.id.toLowerCase().includes(query) || + s.name.toLowerCase().includes(query) || + s.enrolledCourses.some(c => c.toLowerCase().includes(query)) + ) + : [...students]; + + // Sort + if (sortConfig) { + const { column, direction } = sortConfig; + const multiplier = direction === 'asc' ? 1 : -1; + + if (column === 'enrolledStudents') { + filteredCourses.sort((a, b) => (a.enrolledStudents - b.enrolledStudents) * multiplier); + } else if (column === 'capacity') { + filteredClassrooms.sort((a, b) => (a.capacity - b.capacity) * multiplier); + } else if (column === 'studentId') { + filteredStudents.sort((a, b) => a.id.localeCompare(b.id) * multiplier); + } + } + + return { filteredCourses, filteredClassrooms, filteredStudents }; + }; + + const renderTable = () => { + const { filteredCourses, filteredClassrooms, filteredStudents } = getFilteredData(); + const hasSearchQuery = searchQuery.trim().length > 0; + + if (activeTab === 'courses') { + return ( +
+ + + + + + + + + + + + {filteredCourses.map((c) => ( + + + + + + + ))} + {filteredCourses.length === 0 && ( + + + + )} + +
{t('dataInput.courseCode')}{t('dataInput.courseName')} toggleSort('enrolledStudents')} + > + + {t('dataInput.enrolledStudents')} + + + {t('common.actions')}
{c.code}{c.name}{c.enrolledStudents} handleEditClick(c)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noCourses')} +
+
+ ); + } + if (activeTab === 'classrooms') { + return ( +
+ + + + + + + + + + + + {filteredClassrooms.map((r) => ( + + + + + + + ))} + {filteredClassrooms.length === 0 && ( + + + + )} + +
{t('dataInput.roomName')}{t('dataInput.building')} toggleSort('capacity')} + > + + {t('dataInput.capacity')} + + + {t('common.actions')}
{r.name}{r.building}{r.capacity} handleEditClick(r)} + + > + {t('common.edit')} +
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noClassrooms')} +
+
+ ); + } + if (activeTab === 'students') { + const displayStudents = filteredStudents.slice(0, 100); + return ( +
+
+ + + + + + + + + + + + {displayStudents.map((s) => ( + + + + + + + + ))} + {filteredStudents.length === 0 && ( + + + + )} + +
toggleSort('studentId')} + > + + {t('dataInput.studentId')} + + + {t('dataInput.studentNameGenerated')}{t('dataInput.email')}{t('dataInput.enrolledCourses')}{t('common.actions')}
{s.id}{s.name}{s.email} + {s.enrolledCourses.length > 0 ? ( +
+ {s.enrolledCourses.slice(0, 3).map(c => ( + {c} + ))} + {s.enrolledCourses.length > 3 && (+{s.enrolledCourses.length - 3} more)} +
+ ) : (None)} +
handleEditClick(s)}>{t('common.edit')}
+ {hasSearchQuery ? t('dataInput.noSearchResults', { query: searchQuery }) : t('dataInput.noStudents')} +
+
+ {filteredStudents.length > 100 && ( +
+ {t('dataInput.showingStudents', { count: filteredStudents.length })} +
+ )} +
+ ); + } + return null; + }; + + return ( +
+ {isModalOpen && editingItem && ( + setIsModalOpen(false)} + onSave={handleSaveItem} + /> + )} + + {isStudentImportModalOpen && ( + setIsStudentImportModalOpen(false)} + onSelectStudentInfo={handleStudentInfoImport} + onSelectAttendance={handleAttendanceImport} + studentInfoStatus={studentInfoStatus} + attendanceStatus={attendanceStatus} + onSave={saveMergedData} + /> + )} + + + + + + + +
+
+

{t('common.dataManagement')}

+

{t('dataInput.dataManagementDescription')}

+
+ + {/* Search Input */} +
+
+
+ + + + +
+ setSearchQuery(e.target.value)} + placeholder={t('dataInput.searchPlaceholder', { tab: t(`dataInput.${activeTab}`) })} + className="w-full pl-10 pr-10 py-2 border border-slate-300 rounded-lg text-sm text-slate-900 placeholder-slate-400 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all" + /> + {searchQuery && ( + + )} +
+
+ +
+
+ + {isClearMenuOpen && ( + <> +
setIsClearMenuOpen(false)}>
+
+ + + +
+ +
+ + )} +
+ + +
+
+ +
+ {(['courses', 'classrooms', 'students'] as Tab[]).map((tab) => ( + + ))} +
+ +
+ {renderTable()} +
+
+ ); +}; diff --git a/.history/src/components/ScheduleView_20251219212939.tsx b/.history/src/components/ScheduleView_20251219212939.tsx new file mode 100644 index 0000000..8b2b9ab --- /dev/null +++ b/.history/src/components/ScheduleView_20251219212939.tsx @@ -0,0 +1,429 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // Group sessions by time slot (sessions with the same start time) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + filteredSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [filteredSessions]); + + // Handle time slot card click + const handleTimeSlotClick = (exams: ExamSession[]) => { + if (exams.length > 0) { + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 64; + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
+
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + const isMultipleExams = examsInSlot.length > 1; + const firstExam = examsInSlot[0]; + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleExams ? ( + /* Multiple exams - show count and summary */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single exam - show details directly */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ {filterMode !== 'room' && ( +
+ + {room?.name} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; diff --git a/.history/src/components/ScheduleView_20251220162322.tsx b/.history/src/components/ScheduleView_20251220162322.tsx new file mode 100644 index 0000000..d7df95a --- /dev/null +++ b/.history/src/components/ScheduleView_20251220162322.tsx @@ -0,0 +1,454 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // Group sessions by time slot (sessions with the same start time) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + filteredSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [filteredSessions]); + // Helper: Aynı ders ve aynı saatte olan sınavları (Split Exams) tek satırda birleştirir. +const groupExamsForDisplay = (flatSchedule: any[]) => { + const groupedMap = new Map(); + + flatSchedule.forEach((session) => { + // Benzersiz Anahtar: Ders Kodu + Başlangıç Saati + const key = `${session.courseId}-${session.startTime}`; + + if (!groupedMap.has(key)) { + // İlk kez karşılaşıyorsak ana yapıyı kuralım + groupedMap.set(key, { + ...session, + classrooms: [{ name: session.classroomId, count: session.studentCount }], // Diziye çeviriyoruz + totalStudents: session.studentCount + }); + } else { + // Zaten varsa (yani split edilmiş diğer parça geldiyse), sadece sınıfı ekle + const existing = groupedMap.get(key); + existing.classrooms.push({ name: session.classroomId, count: session.studentCount }); + existing.totalStudents += session.studentCount; + } + }); + + return Array.from(groupedMap.values()); +}; + + // Handle time slot card click + const handleTimeSlotClick = (exams: ExamSession[]) => { + if (exams.length > 0) { + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 64; + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
+
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + const isMultipleExams = examsInSlot.length > 1; + const firstExam = examsInSlot[0]; + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleExams ? ( + /* Multiple exams - show count and summary */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single exam - show details directly */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ {filterMode !== 'room' && ( +
+ + {room?.name} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; diff --git a/.history/src/components/ScheduleView_20251220162652.tsx b/.history/src/components/ScheduleView_20251220162652.tsx new file mode 100644 index 0000000..ce9562f --- /dev/null +++ b/.history/src/components/ScheduleView_20251220162652.tsx @@ -0,0 +1,403 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// UI için genişletilmiş tip (Split sınavları göstermek için) +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Sınıf listesi + totalStudents?: number; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Adım: Ham filtreleme (Mevcut mantık) + const rawFilteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Adım: Split Sınavları Birleştirme (YENİ EKLENEN MANTIK) + // Eğer 'Room' modunda değilsek, aynı dersin aynı saatteki parçalarını birleştiriyoruz. + const processedSessions = useMemo(() => { + // Oda görünümünde birleştirme yapmamalıyız çünkü sadece o odayı görmek istiyoruz. + if (filterMode === 'room') return rawFilteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + rawFilteredSessions.forEach(session => { + // Ders Kodu + Başlangıç Saati birleşim anahtarıdır + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: session.studentCount || 0 }], + totalStudents: session.studentCount || 0 + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; // Bu artık bölünmüş bir sınav + existing.classroomList?.push({ name: session.classroomId, count: session.studentCount || 0 }); + existing.totalStudents = (existing.totalStudents || 0) + (session.studentCount || 0); + } + }); + + return Array.from(groupedMap.values()); + }, [rawFilteredSessions, filterMode]); + + // 3. Adım: Zaman dilimine göre gruplama (Processed sessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal'a ham veriyi değil, gerekirse açılmış veriyi gönderebiliriz + // Ancak şimdilik özet gösterimi yeterli + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Sınıf listesi sığsın diye yüksekliği biraz artırdım (64 -> 80) + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+

{t('dashboard.noSchedule')}

+
+ ); + } + + return ( +
+ {/* Toast Message */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + + {/* Header Controls */} +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ )} +
+ +
+ + +
+ + + + +
+
+
+ + {/* Schedule Grid */} +
+
{/* Genişliği artırdım */} +
+
Time
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+ {/* Time Column */} +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ + {/* Days Columns */} + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Eğer aynı saatte birden fazla FARKLI ders varsa (Örn: Math ve Physics) + const isMultipleDifferentExams = examsInSlot.length > 1; + const firstExam = examsInSlot[0]; + const course = courses.find(c => c.id === firstExam.courseId); + + // Tek bir ders var ama Split edilmişse + const isSplitExam = firstExam.isSplit; + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* --- SENARYO 1: AYNI SAATTE FARKLI DERSLER --- */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map((exam, idx) => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} +
+ + ) : ( + /* --- SENARYO 2: TEK DERS (Split veya Normal) --- */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT SINAV GÖSTERİMİ --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL SINAV GÖSTERİMİ --- +
+ + {classrooms.find(r => r.id === firstExam.classroomId)?.name || firstExam.classroomId} + {firstExam.studentCount && ({firstExam.studentCount})} +
+ )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220164516.tsx b/.history/src/components/ScheduleView_20251220164516.tsx new file mode 100644 index 0000000..5a95e65 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220164516.tsx @@ -0,0 +1,430 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // Group sessions by time slot (sessions with the same start time) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + filteredSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [filteredSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: ExamSession[]) => { + if (exams.length > 0) { + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 64; + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
+
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + const isMultipleExams = examsInSlot.length > 1; + const firstExam = examsInSlot[0]; + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleExams ? ( + /* Multiple exams - show count and summary */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single exam - show details directly */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ {filterMode !== 'room' && ( +
+ + {room?.name} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; diff --git a/.history/src/components/ScheduleView_20251220164946.tsx b/.history/src/components/ScheduleView_20251220164946.tsx new file mode 100644 index 0000000..5d95c22 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220164946.tsx @@ -0,0 +1,497 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// Backend'den gelen veriyi UI'da birleştirmek için genişletilmiş tip +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220171612.tsx b/.history/src/components/ScheduleView_20251220171612.tsx new file mode 100644 index 0000000..ccc9f48 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220171612.tsx @@ -0,0 +1,502 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// Backend'den gelen veriyi UI'da birleştirmek için genişletilmiş tip +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + useEffect(() => { + console.log("İşlenmiş Veriler:", processedSessions); + const splitExams = processedSessions.filter(s => s.isSplit); + console.log("Bölünmüş Sınav Sayısı:", splitExams.length); +}, [processedSessions]); + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220171643.tsx b/.history/src/components/ScheduleView_20251220171643.tsx new file mode 100644 index 0000000..02839e3 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220171643.tsx @@ -0,0 +1,498 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// Backend'den gelen veriyi UI'da birleştirmek için genişletilmiş tip +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220172044.tsx b/.history/src/components/ScheduleView_20251220172044.tsx new file mode 100644 index 0000000..19a2129 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220172044.tsx @@ -0,0 +1,504 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// Backend'den gelen veriyi UI'da birleştirmek için genişletilmiş tip +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + useEffect(() => { + console.log("İşlenmiş Veriler:", processedSessions); + const splitExams = processedSessions.filter(s => s.isSplit); + console.log("Bölünmüş Sınav Sayısı:", splitExams.length); +}, [processedSessions]); + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220172349.tsx b/.history/src/components/ScheduleView_20251220172349.tsx new file mode 100644 index 0000000..02839e3 --- /dev/null +++ b/.history/src/components/ScheduleView_20251220172349.tsx @@ -0,0 +1,498 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +// Backend'den gelen veriyi UI'da birleştirmek için genişletilmiş tip +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251220183441.tsx b/.history/src/components/ScheduleView_20251220183441.tsx new file mode 100644 index 0000000..466758c --- /dev/null +++ b/.history/src/components/ScheduleView_20251220183441.tsx @@ -0,0 +1,490 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + const sessionsForExport = schedule.map(s => ({ + sessionId: s.id, + courseCode: s.courseId, + classroomName: s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + })); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+ {course?.code} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- +
+ {firstExam.classroomList?.map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count}) + + ))} +
+ ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount ? ({firstExam.studentCount}) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251221141002.tsx b/.history/src/components/ScheduleView_20251221141002.tsx new file mode 100644 index 0000000..5347b01 --- /dev/null +++ b/.history/src/components/ScheduleView_20251221141002.tsx @@ -0,0 +1,543 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + // 1. ADIM: Veriyi Sırala (Tarih ve Ders Koduna Göre) + // Bu işlem, CSV çıktısının Excel'de "düzenli" (tarih sırasına göre) görünmesini sağlar. + // Ayrıca aynı derslerin alt alta gelmesini garantileyerek aşağıdaki "Exam 1, Exam 1" mantığını mümkün kılar. + const sortedSchedule = [...schedule].sort((a, b) => { + const timeDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + if (timeDiff !== 0) return timeDiff; + return a.courseId.localeCompare(b.courseId); + }); + + // 2. ADIM: Akıllı Numaralandırma (Grouping) + let examCounter = 0; + let lastKey = ''; // Önceki satırın "Ders + Saat" bilgisini tutar + + const sessionsForExport = sortedSchedule.map(s => { + const course = courses.find(c => c.id === s.courseId); + const classroom = classrooms.find(c => c.id === s.classroomId); + + // Anahtar: Ders ID + Başlangıç Saati + // Eğer bu satır, bir önceki satırla aynı ders ve aynı saatteyse (Split Exam), aynı sınavdır. + const currentKey = `${s.courseId}-${new Date(s.startTime).toISOString()}`; + + // Sadece ders veya saat değiştiğinde sayacı artır + if (currentKey !== lastKey) { + examCounter++; + lastKey = currentKey; + } + + return { + // Backend'den gelen karışık ID yerine, bizim ürettiğimiz sıralı "Exam X" numarasını kullanıyoruz. + sessionId: `Exam ${examCounter}`, + courseCode: course?.code || s.courseId, + classroomName: classroom?.name || s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + // TypeScript hatası almamak için güvenli erişim (studentCount backend'den geliyorsa) + studentCount: (s as any).studentCount || 0 + }; + }); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name, capacity: c.capacity })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+
+ {course?.code} +
+ {isSplitExam && ( + + {t('schedule.split', 'Split')} + + )} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- + <> +
+ + {t('schedule.totalStudents', { count: firstExam.totalStudents })} +
+
+ {(firstExam.classroomList || []).slice(0, 2).map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count} / {classrooms.find(r => r.id === cr.name)?.capacity || '?'}) + + ))} + {firstExam.classroomList && firstExam.classroomList.length > 2 && ( + + +{firstExam.classroomList.length - 2} {t('common.more')} + + )} +
+ + ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount && room?.capacity ? ( + ({firstExam.studentCount} / {room.capacity}) + ) : firstExam.studentCount ? ( + ({firstExam.studentCount}) + ) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251221141215.tsx b/.history/src/components/ScheduleView_20251221141215.tsx new file mode 100644 index 0000000..3b3caa5 --- /dev/null +++ b/.history/src/components/ScheduleView_20251221141215.tsx @@ -0,0 +1,540 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; + + const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + + const sortedSchedule = [...schedule].sort((a, b) => { + const timeDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + if (timeDiff !== 0) return timeDiff; + return a.courseId.localeCompare(b.courseId); + }); + + + let examCounter = 0; + let lastKey = ''; + + const sessionsForExport = sortedSchedule.map(s => { + const course = courses.find(c => c.id === s.courseId); + const classroom = classrooms.find(c => c.id === s.classroomId); + + + const currentKey = `${s.courseId}-${new Date(s.startTime).toISOString()}`; + + + if (currentKey !== lastKey) { + examCounter++; + lastKey = currentKey; + } + + return { + + sessionId: `Exam ${examCounter}`, + courseCode: course?.code || s.courseId, + classroomName: classroom?.name || s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + + studentCount: (s as any).studentCount || 0 + }; + }); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name, capacity: c.capacity })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+
+ {course?.code} +
+ {isSplitExam && ( + + {t('schedule.split', 'Split')} + + )} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- + <> +
+ + {t('schedule.totalStudents', { count: firstExam.totalStudents })} +
+
+ {(firstExam.classroomList || []).slice(0, 2).map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count} / {classrooms.find(r => r.id === cr.name)?.capacity || '?'}) + + ))} + {firstExam.classroomList && firstExam.classroomList.length > 2 && ( + + +{firstExam.classroomList.length - 2} {t('common.more')} + + )} +
+ + ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount && room?.capacity ? ( + ({firstExam.studentCount} / {room.capacity}) + ) : firstExam.studentCount ? ( + ({firstExam.studentCount}) + ) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251221141433.tsx b/.history/src/components/ScheduleView_20251221141433.tsx new file mode 100644 index 0000000..95d6928 --- /dev/null +++ b/.history/src/components/ScheduleView_20251221141433.tsx @@ -0,0 +1,545 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; +const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + // 1. ADIM: Listeyi Sırala (Önce Tarih, Sonra Ders Kodu) + // Bu, CSV dosyasının Excel'de düzenli görünmesini sağlar. + const sortedSchedule = [...schedule].sort((a, b) => { + // Tarihe göre artan sıralama + const timeDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + if (timeDiff !== 0) return timeDiff; + + // Tarihler aynıysa ders koduna göre alfabetik sıralama + // Bu sayede split edilmiş parçalar (Aynı tarih, aynı ders) alt alta gelir. + return a.courseId.localeCompare(b.courseId); + }); + + // 2. ADIM: Gruplama ve Numaralandırma + let examCounter = 0; + let lastKey = ''; // Önceki satırın (Ders + Saat) bilgisini tutar + + const sessionsForExport = sortedSchedule.map(s => { + const course = courses.find(c => c.id === s.courseId); + const classroom = classrooms.find(c => c.id === s.classroomId); + + // Anahtar: Ders ID + Başlangıç Saati + const currentKey = `${s.courseId}-${new Date(s.startTime).toISOString()}`; + + // Eğer bu satır, bir önceki satırla aynı ders ve aynı saatteyse (Split Exam), aynı sınavdır. + // Farklıysa sayacı artır. + if (currentKey !== lastKey) { + examCounter++; + lastKey = currentKey; + } + + return { + // Backend'den gelen ID yerine, bizim sıralı ve temiz numaramız + sessionId: `Exam ${examCounter}`, + courseCode: course?.code || s.courseId, + classroomName: classroom?.name || s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + // TypeScript için güvenli erişim + studentCount: (s as any).studentCount || 0 + }; + }); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name, capacity: c.capacity })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+
+ {course?.code} +
+ {isSplitExam && ( + + {t('schedule.split', 'Split')} + + )} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- + <> +
+ + {t('schedule.totalStudents', { count: firstExam.totalStudents })} +
+
+ {(firstExam.classroomList || []).slice(0, 2).map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count} / {classrooms.find(r => r.id === cr.name)?.capacity || '?'}) + + ))} + {firstExam.classroomList && firstExam.classroomList.length > 2 && ( + + +{firstExam.classroomList.length - 2} {t('common.more')} + + )} +
+ + ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount && room?.capacity ? ( + ({firstExam.studentCount} / {room.capacity}) + ) : firstExam.studentCount ? ( + ({firstExam.studentCount}) + ) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/ScheduleView_20251221142046.tsx b/.history/src/components/ScheduleView_20251221142046.tsx new file mode 100644 index 0000000..e049e13 --- /dev/null +++ b/.history/src/components/ScheduleView_20251221142046.tsx @@ -0,0 +1,541 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; +import { TimeSlotDetailModal } from './TimeSlotDetailModal'; + +interface ScheduleViewProps { + schedule: ExamSession[]; + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} + +type FilterMode = 'all' | 'room' | 'course' | 'student'; + +export const ScheduleView: React.FC = ({ schedule, courses, classrooms, students }) => { + const { t, i18n } = useTranslation(); + const [filterMode, setFilterMode] = useState('all'); + const [selectedEntityId, setSelectedEntityId] = useState(''); + const [isExporting, setIsExporting] = useState(false); + const [exportMessage, setExportMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + // Time slot detail modal state + const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); + + // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty + const [currentDate, setCurrentDate] = useState(() => { + if (schedule.length > 0) { + return new Date(schedule[0].startTime); + } + return new Date(); + }); + + useEffect(() => { + if (filterMode === 'room' && classrooms.length > 0) setSelectedEntityId(classrooms[0].id); + else if (filterMode === 'course' && courses.length > 0) setSelectedEntityId(courses[0].id); + else if (filterMode === 'student' && students.length > 0) setSelectedEntityId(students[0].id); + }, [filterMode, classrooms, courses, students]); + + // Clear export message after 3 seconds + useEffect(() => { + if (exportMessage) { + const timer = setTimeout(() => setExportMessage(null), 3000); + return () => clearTimeout(timer); + } + }, [exportMessage]); + + const weekStart = useMemo(() => { + const d = new Date(currentDate); + const day = d.getDay(); + // Adjust to Monday start (if day is 0 (Sunday), subtract 6 days, else subtract day-1) + const diff = d.getDate() - day + (day === 0 ? -6 : 1); + d.setDate(diff); + d.setHours(0, 0, 0, 0); + return d; + }, [currentDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const d = new Date(weekStart); + d.setDate(d.getDate() + i); + return d; + }); + }, [weekStart]); + + // 1. Step: Basic Filtering + const filteredSessions = useMemo(() => { + if (filterMode === 'all') return schedule; + + if (!selectedEntityId) return []; + + return schedule.filter(session => { + if (filterMode === 'room') return session.classroomId === selectedEntityId; + if (filterMode === 'course') return session.courseId === selectedEntityId; + if (filterMode === 'student') { + const student = students.find(s => s.id === selectedEntityId); + return student?.enrolledCourses.includes(session.courseId); + } + return false; + }); + }, [schedule, filterMode, selectedEntityId, students]); + + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); + + filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { + const startTime = new Date(session.startTime); + // Create a key from the date and time + const key = startTime.toISOString(); + + if (!groups.has(key)) { + groups.set(key, []); + } + groups.get(key)!.push(session); + }); + + return groups; + }, [processedSessions]); + + + // Handle time slot card click + const handleTimeSlotClick = (exams: DisplaySession[]) => { + if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz + setSelectedTimeSlotExams(exams); + setSelectedTimeSlot({ + start: new Date(exams[0].startTime), + end: new Date(exams[0].endTime), + }); + setShowTimeSlotModal(true); + } + }; + + const START_HOUR = 8; + const END_HOUR = 22; + const HOURS_COUNT = END_HOUR - START_HOUR; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + }; + + const navigateWeek = (direction: 'prev' | 'next') => { + const newDate = new Date(currentDate); + newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7)); + setCurrentDate(newDate); + }; + + const goToToday = () => { + setCurrentDate(new Date()); + }; + + const handleDateChange = (e: React.ChangeEvent) => { + if (e.target.value) { + setCurrentDate(new Date(e.target.value)); + } + }; +const handleExport = async () => { + setIsExporting(true); + setExportMessage(null); + + try { + + const sortedSchedule = [...schedule].sort((a, b) => { + + const timeDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + if (timeDiff !== 0) return timeDiff; + + + return a.courseId.localeCompare(b.courseId); + }); + + let examCounter = 0; + let lastKey = ''; + + const sessionsForExport = sortedSchedule.map(s => { + const course = courses.find(c => c.id === s.courseId); + const classroom = classrooms.find(c => c.id === s.classroomId); + + + const currentKey = `${s.courseId}-${new Date(s.startTime).toISOString()}`; + + + if (currentKey !== lastKey) { + examCounter++; + lastKey = currentKey; + } + + return { + + sessionId: `Exam ${examCounter}`, + courseCode: course?.code || s.courseId, + classroomName: classroom?.name || s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + + studentCount: (s as any).studentCount || 0 + }; + }); + + const result = await window.api.exportScheduleCSV({ + sessions: sessionsForExport, + courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name, capacity: c.capacity })), + }); + + if (result.success) { + setExportMessage({ type: 'success', text: result.message }); + } else { + setExportMessage({ type: 'error', text: result.message }); + } + } catch (error) { + setExportMessage({ type: 'error', text: `Export failed: ${error instanceof Error ? error.message : 'Unknown error'}` }); + } finally { + setIsExporting(false); + } + }; + + if (schedule.length === 0) { + return ( +
+ + + +

{t('dashboard.noSchedule')}

+

{t('schedule.goToDashboard')}

+
+ ); + } + + return ( +
+ {/* Export message toast */} + {exportMessage && ( +
+ {exportMessage.text} +
+ )} + +
+
+
+ {(['all', 'room', 'student', 'course'] as FilterMode[]).map((m) => ( + + ))} +
+ + {filterMode !== 'all' && ( +
+ +
+ +
+
+ )} +
+ +
+ + + {/* Navigation Controls */} +
+ + + + +
+
+
+ +
+
{/* Increased width for split badges */} +
+
+ GMT+0 +
+ {weekDays.map((day, i) => ( +
+
+ {day.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short' })} +
+
+ {day.getDate()} +
+
+ ))} +
+ +
+
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ {(START_HOUR + i).toString().padStart(2, '0')}:00 +
+ ))} +
+ {END_HOUR}:00 +
+
+ + {weekDays.map((day, colIndex) => ( +
+ {Array.from({ length: HOURS_COUNT }, (_, i) => ( +
+ ))} + + {/* Render grouped time slots */} + {Array.from(groupedSessionsByTimeSlot.entries()) + .filter(([key]) => new Date(key).toDateString() === day.toDateString()) + .map(([timeKey, examsInSlot]) => { + const start = new Date(timeKey); + const end = new Date(examsInSlot[0].endTime); + + const startMinutes = (start.getHours() * 60 + start.getMinutes()) - (START_HOUR * 60); + const durationMinutes = (end.getTime() - start.getTime()) / (1000 * 60); + + const top = (startMinutes / 60) * HOUR_HEIGHT; + const height = (durationMinutes / 60) * HOUR_HEIGHT; + + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) + const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + + const course = courses.find(c => c.id === firstExam.courseId); + const room = classrooms.find(r => r.id === firstExam.classroomId); + + return ( +
handleTimeSlotClick(examsInSlot)} + > +
+ {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ + <> +
+
+ {examsInSlot.length} +
+ + {t('schedule.multipleExams', { count: examsInSlot.length })} + +
+
+ {examsInSlot.slice(0, 2).map(exam => ( +
+ • {courses.find(c => c.id === exam.courseId)?.code || exam.courseId} +
+ ))} + {examsInSlot.length > 2 && ( +
+{examsInSlot.length - 2} more...
+ )} +
+
+ + + + + + {t('schedule.clickForDetails') || 'Click for details'} +
+ + ) : ( + /* Single Course (Split or Normal) */ + <> +
+
+ {course?.code} +
+ {isSplitExam && ( + + {t('schedule.split', 'Split')} + + )} +
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- + <> +
+ + {t('schedule.totalStudents', { count: firstExam.totalStudents })} +
+
+ {(firstExam.classroomList || []).slice(0, 2).map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count} / {classrooms.find(r => r.id === cr.name)?.capacity || '?'}) + + ))} + {firstExam.classroomList && firstExam.classroomList.length > 2 && ( + + +{firstExam.classroomList.length - 2} {t('common.more')} + + )} +
+ + ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount && room?.capacity ? ( + ({firstExam.studentCount} / {room.capacity}) + ) : firstExam.studentCount ? ( + ({firstExam.studentCount}) + ) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + + )} +
+ + )} +
+
+ ); + })} +
+ ))} +
+
+
+ + {/* Time Slot Detail Modal */} + setShowTimeSlotModal(false)} + exams={selectedTimeSlotExams} + courses={courses} + classrooms={classrooms} + timeSlot={selectedTimeSlot} + /> +
+ ); +}; \ No newline at end of file diff --git a/.history/src/components/TimeSlotDetailModal_20251221182740.tsx b/.history/src/components/TimeSlotDetailModal_20251221182740.tsx new file mode 100644 index 0000000..e2a8b96 --- /dev/null +++ b/.history/src/components/TimeSlotDetailModal_20251221182740.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { DisplaySession, Course, Classroom } from '../types'; // Use DisplaySession + +interface TimeSlotDetailModalProps { + isOpen: boolean; + onClose: () => void; + exams: DisplaySession[]; // Changed from ExamSession[] + courses: Course[]; + classrooms: Classroom[]; + timeSlot: { start: Date; end: Date } | null; +} + +export const TimeSlotDetailModal: React.FC = ({ + isOpen, + onClose, + exams, + courses, + classrooms, + timeSlot, +}) => { + const { t, i18n } = useTranslation(); + + if (!isOpen || !timeSlot) return null; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { + hour: '2-digit', + minute: '2-digit', + }); + }; + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + // This creates a more visually distinct set of colors + const a = [ + { bg: 'bg-ieu-50', border: 'border-ieu-300', text: 'text-ieu-800' }, + { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-800' }, + { bg: 'bg-sky-50', border: 'border-sky-300', text: 'text-sky-800' }, + { bg: 'bg-amber-50', border: 'border-amber-300', text: 'text-amber-800' }, + { bg: 'bg-rose-50', border: 'border-rose-300', text: 'text-rose-800' }, + { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-800' }, + { bg: 'bg-cyan-50', border: 'border-cyan-300', text: 'text-cyan-800' }, + { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-800' }, + ] + + return ReactDOM.createPortal( +
+
e.stopPropagation()} + > + {/* Header */} +
+======= + className="bg-white rounded-xl shadow-2xl w-full max-w-2xl max-h-[80vh] flex flex-col" + onClick={(e) => e.stopPropagation()} + > + {/* Header */} +
+>>>>>>> origin/main +
+
+

+ {t('schedule.timeSlotDetails')} +

+

+ {formatDate(timeSlot.start)} + | + {formatTime(timeSlot.start)} - {formatTime(timeSlot.end)} +

+
+ +
+
+ + {/* Exam List */} +<<<<<<< HEAD +
+ {exams.map((exam) => { +======= +
+ {exams.map((exam, index) => { +>>>>>>> origin/main + const course = courses.find(c => c.id === exam.courseId); + + return ( +
+ {/* Course Info Header */} +
+

+ {course?.code || exam.courseId} +

+

+ {course?.name || t('common.noData')} +

+
+ + {/* Body (SPLIT LOGIC HERE) */} +
+ {exam.isSplit ? ( + // --- RENDER SPLIT EXAM --- +
+
+

+ {t('schedule.splitClassrooms')} +

+ + {t('schedule.enrolledCount', { count: exam.totalStudents || course?.enrolledStudents || 0 })} + +
+
+ {exam.classroomList?.map((cr, index) => { + const classroom = classrooms.find(c => c.id === cr.name); + return ( +
+
+ + {classroom?.name || cr.name} + ({t('schedule.capacityShort', { capacity: classroom?.capacity || 0 })}) +
+ + {t('schedule.studentCount', { count: cr.count })} + +
+ ) + })} +
+
+ ) : ( + // --- RENDER NORMAL EXAM --- +
+
+ + {classrooms.find(c=>c.id === exam.classroomId)?.name || exam.classroomId} +
+
+ + {t('schedule.enrolledCount', { count: exam.studentCount || course?.enrolledStudents || 0 })} +
+
+ )} +
+
+ ); + })} +
+ + {/* Footer */} +<<<<<<< HEAD +
+======= +
+>>>>>>> origin/main + +
+
+
, + document.body + ); +}; \ No newline at end of file diff --git a/.history/src/components/TimeSlotDetailModal_20251221183128.tsx b/.history/src/components/TimeSlotDetailModal_20251221183128.tsx new file mode 100644 index 0000000..47232b2 --- /dev/null +++ b/.history/src/components/TimeSlotDetailModal_20251221183128.tsx @@ -0,0 +1,170 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { DisplaySession, Course, Classroom } from '../types'; // Use DisplaySession + +interface TimeSlotDetailModalProps { + isOpen: boolean; + onClose: () => void; + exams: DisplaySession[]; // Changed from ExamSession[] + courses: Course[]; + classrooms: Classroom[]; + timeSlot: { start: Date; end: Date } | null; +} + +export const TimeSlotDetailModal: React.FC = ({ + isOpen, + onClose, + exams, + courses, + classrooms, + timeSlot, +}) => { + const { t, i18n } = useTranslation(); + + if (!isOpen || !timeSlot) return null; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { + hour: '2-digit', + minute: '2-digit', + }); + }; + + const formatDate = (date: Date) => { + return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }); + }; + + // This creates a more visually distinct set of colors + const a = [ + { bg: 'bg-ieu-50', border: 'border-ieu-300', text: 'text-ieu-800' }, + { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-800' }, + { bg: 'bg-sky-50', border: 'border-sky-300', text: 'text-sky-800' }, + { bg: 'bg-amber-50', border: 'border-amber-300', text: 'text-amber-800' }, + { bg: 'bg-rose-50', border: 'border-rose-300', text: 'text-rose-800' }, + { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-800' }, + { bg: 'bg-cyan-50', border: 'border-cyan-300', text: 'text-cyan-800' }, + { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-800' }, + ] + + return ReactDOM.createPortal( +
+
e.stopPropagation()} + > + {/* Header */} +
+
+
+

+ {t('schedule.timeSlotDetails')} +

+

+ {formatDate(timeSlot.start)} + | + {formatTime(timeSlot.start)} - {formatTime(timeSlot.end)} +

+
+ +
+
+ + {/* Exam List */} +
+ {exams.map((exam, index) => { + const course = courses.find(c => c.id === exam.courseId); + + return ( +
+ {/* Course Info Header */} +
+

+ {course?.code || exam.courseId} +

+

+ {course?.name || t('common.noData')} +

+
+ + {/* Body (SPLIT LOGIC HERE) */} +
+ {exam.isSplit ? ( + // --- RENDER SPLIT EXAM --- +
+
+

+ {t('schedule.splitClassrooms')} +

+ + {t('schedule.enrolledCount', { count: exam.totalStudents || course?.enrolledStudents || 0 })} + +
+
+ {exam.classroomList?.map((cr, index) => { + const classroom = classrooms.find(c => c.id === cr.name); + return ( +
+
+ + {classroom?.name || cr.name} + ({t('schedule.capacityShort', { capacity: classroom?.capacity || 0 })}) +
+ + {t('schedule.studentCount', { count: cr.count })} + +
+ ) + })} +
+
+ ) : ( + // --- RENDER NORMAL EXAM --- +
+
+ + {classrooms.find(c=>c.id === exam.classroomId)?.name || exam.classroomId} +
+
+ + {t('schedule.enrolledCount', { count: exam.studentCount || course?.enrolledStudents || 0 })} +
+
+ )} +
+
+ ); + })} +
+ + {/* Footer */} +
+ +
+
+
, + document.body + ); +}; \ No newline at end of file diff --git a/.history/src/types/index_20251219212939.ts b/.history/src/types/index_20251219212939.ts new file mode 100644 index 0000000..d84c489 --- /dev/null +++ b/.history/src/types/index_20251219212939.ts @@ -0,0 +1,52 @@ +export enum ViewMode { + DASHBOARD = 'Dashboard', + DATA = 'Data Management', + SCHEDULE = 'Schedule View', + SETTINGS = 'Settings', +} + +export interface GenerationConstraints { + startDate: Date; + endDate: Date; + includeWeekends: boolean; + dailyStartTime: string; // "09:00" + dailyEndTime: string; // "17:00" + maxExamsPerDay: number; // Maximum exams per student per day (default: 2) + allowConsecutiveExams: boolean; // Allow back-to-back exams (default: true) + minHoursBetweenExams: number; // Minimum hours between exams for a student (default: 1) +} + +export interface Course { + id: string; + code: string; + name: string; + enrolledStudents: number; +} + +export interface Classroom { + id: string; + name: string; + capacity: number; + building: string; +} + +export interface Student { + id: string; + name: string; + email: string; + enrolledCourses: string[]; // Array of Course IDs +} + +export interface ExamSession { + id: string; + courseId: string; + classroomId: string; + startTime: Date; + endTime: Date; +} + +export interface DataContextType { + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} diff --git a/.history/src/types/index_20251220175355.ts b/.history/src/types/index_20251220175355.ts new file mode 100644 index 0000000..47d750d --- /dev/null +++ b/.history/src/types/index_20251220175355.ts @@ -0,0 +1,58 @@ +export enum ViewMode { + DASHBOARD = 'Dashboard', + DATA = 'Data Management', + SCHEDULE = 'Schedule View', + SETTINGS = 'Settings', +} + +export interface GenerationConstraints { + startDate: Date; + endDate: Date; + includeWeekends: boolean; + dailyStartTime: string; // "09:00" + dailyEndTime: string; // "17:00" + maxExamsPerDay: number; // Maximum exams per student per day (default: 2) + allowConsecutiveExams: boolean; // Allow back-to-back exams (default: true) + minHoursBetweenExams: number; // Minimum hours between exams for a student (default: 1) +} + +export interface Course { + id: string; + code: string; + name: string; + enrolledStudents: number; +} + +export interface Classroom { + id: string; + name: string; + capacity: number; + building: string; +} + +export interface Student { + id: string; + name: string; + email: string; + enrolledCourses: string[]; // Array of Course IDs +} + +export interface ExamSession { + id: string; + courseId: string; + classroomId: string; + startTime: Date; + endTime: Date; +} + +export interface DataContextType { + courses: Course[]; + classrooms: Classroom[]; + students: Student[]; +} +interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // Birleşmiş sınıf listesi + totalStudents?: number; + studentCount?: number; // Backend'den gelen ham veri +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..35fe170 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "CodeGPT.apiKey": "Ollama" +} \ No newline at end of file diff --git a/generate_sample_data.js b/generate_sample_data.js deleted file mode 100644 index f8cfad5..0000000 --- a/generate_sample_data.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Generate sample data with CONTROLLED overlaps - * Courses within same department share students (sequential scheduling) - * Courses in different departments have NO overlap (parallel scheduling possible) - */ - -const fs = require('fs'); -const path = require('path'); - -// Configuration - smaller but demonstrates parallel scheduling -const DEPARTMENTS = ['CS', 'MATH', 'PHYS', 'CHEM', 'BIO']; -const COURSES_PER_DEPT = 10; // 50 total courses -const STUDENTS_PER_DEPT = 200; // 1000 total students (no overlap between depts) -const COURSES_PER_STUDENT = 4; // Within their department only -const NUM_ROOMS = 20; - -// Generate courses -function generateCourses() { - const courses = []; - for (const dept of DEPARTMENTS) { - for (let i = 1; i <= COURSES_PER_DEPT; i++) { - courses.push(`${dept}${100 + i}`); - } - } - return courses; -} - -// Generate rooms -function generateRooms() { - const rooms = []; - const buildings = ['A', 'B', 'C', 'D']; - for (let i = 0; i < NUM_ROOMS; i++) { - const building = buildings[i % buildings.length]; - const roomNum = 100 + i; - const capacity = 50 + (i % 5) * 20; // 50-130 capacity - rooms.push({ name: `${building}${roomNum}`, capacity }); - } - return rooms; -} - -// Generate enrollments - students ONLY in their department (NO cross-dept overlap) -function generateEnrollments() { - const enrollments = new Map(); - let studentNum = 1; - - for (const dept of DEPARTMENTS) { - // Get courses in this department - const deptCourses = []; - for (let i = 1; i <= COURSES_PER_DEPT; i++) { - const code = `${dept}${100 + i}`; - deptCourses.push(code); - enrollments.set(code, []); - } - - // Create students for this department only - for (let s = 0; s < STUDENTS_PER_DEPT; s++) { - const studentId = `STD${String(studentNum++).padStart(5, '0')}`; - - // Pick random courses from THIS department only - const shuffled = [...deptCourses].sort(() => Math.random() - 0.5); - const selectedCourses = shuffled.slice(0, COURSES_PER_STUDENT); - - for (const course of selectedCourses) { - enrollments.get(course).push(studentId); - } - } - } - - return enrollments; -} - -// Write files -function writeFiles(courses, rooms, enrollments) { - const outputDir = path.join(__dirname, 'sample_data'); - if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); - - // Courses - fs.writeFileSync( - path.join(outputDir, 'courses.csv'), - 'ALL OF THE COURSES IN THE SYSTEM\r\n' + courses.join('\r\n') - ); - - // Rooms - fs.writeFileSync( - path.join(outputDir, 'classrooms.csv'), - 'ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM\r\n' + - rooms.map(r => `${r.name};${r.capacity}`).join('\r\n') - ); - - // Attendance - let attendance = ''; - for (const [course, students] of enrollments) { - attendance += `${course}\r\n`; - attendance += `[${students.map(s => `'${s}'`).join(', ')}]\r\n\r\n`; - } - fs.writeFileSync(path.join(outputDir, 'attendance.csv'), attendance); - - console.log(`✅ Generated sample data with PARALLEL scheduling capability:`); - console.log(` ${courses.length} courses (${COURSES_PER_DEPT} per department)`); - console.log(` ${rooms.length} rooms`); - console.log(` ${STUDENTS_PER_DEPT * DEPARTMENTS.length} students`); - console.log(` Students only enrolled in their own department → NO cross-dept conflicts!`); - console.log(` → Courses from different departments CAN be at the same time`); -} - -const courses = generateCourses(); -const rooms = generateRooms(); -const enrollments = generateEnrollments(); -writeFiles(courses, rooms, enrollments); diff --git a/package-lock.json b/package-lock.json index bfaee4a..371f884 100644 --- a/package-lock.json +++ b/package-lock.json @@ -135,6 +135,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -749,6 +750,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -772,6 +774,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1229,6 +1232,7 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -2322,6 +2326,7 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -4109,8 +4114,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4449,6 +4453,7 @@ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4459,6 +4464,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4576,6 +4582,7 @@ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5233,6 +5240,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6093,6 +6101,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7241,8 +7250,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ds-store": { "version": "0.1.6", @@ -7908,31 +7916,6 @@ "license": "MIT", "optional": true }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -8239,6 +8222,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9749,6 +9733,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -10617,6 +10602,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -11705,6 +11691,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -12474,7 +12461,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -13866,6 +13852,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13966,7 +13953,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -13982,7 +13968,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -13995,8 +13980,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/proc-log": { "version": "2.0.1", @@ -14167,6 +14151,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14176,6 +14161,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14214,13 +14200,15 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz", "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-redux": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -14490,7 +14478,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -14962,6 +14951,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16513,6 +16503,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16902,6 +16893,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -17028,6 +17020,7 @@ "integrity": "sha512-HU1JOuV1OavsZ+mfigY0j8d1TgQgbZ6M+J75zDkpEAwYeXjWSqrGJtgnPblJjd/mAyTNQ7ygw0MiKOn6etz8yw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/sample_data/Yeni Metin Belgesi.txt b/sample_data/Yeni Metin Belgesi.txt new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/attendance.csv b/sample_data/attendance.csv deleted file mode 100644 index e1f5195..0000000 --- a/sample_data/attendance.csv +++ /dev/null @@ -1,150 +0,0 @@ -CS101 -['STD00002', 'STD00003', 'STD00007', 'STD00008', 'STD00013', 'STD00014', 'STD00015', 'STD00016', 'STD00018', 'STD00024', 'STD00025', 'STD00026', 'STD00028', 'STD00029', 'STD00031', 'STD00032', 'STD00037', 'STD00039', 'STD00041', 'STD00042', 'STD00043', 'STD00046', 'STD00047', 'STD00049', 'STD00050', 'STD00051', 'STD00052', 'STD00054', 'STD00055', 'STD00060', 'STD00063', 'STD00065', 'STD00070', 'STD00073', 'STD00074', 'STD00075', 'STD00076', 'STD00077', 'STD00078', 'STD00081', 'STD00083', 'STD00084', 'STD00086', 'STD00087', 'STD00088', 'STD00089', 'STD00090', 'STD00091', 'STD00092', 'STD00093', 'STD00094', 'STD00098', 'STD00102', 'STD00103', 'STD00104', 'STD00105', 'STD00106', 'STD00107', 'STD00108', 'STD00109', 'STD00111', 'STD00113', 'STD00115', 'STD00116', 'STD00119', 'STD00122', 'STD00124', 'STD00125', 'STD00128', 'STD00129', 'STD00130', 'STD00132', 'STD00133', 'STD00136', 'STD00138', 'STD00143', 'STD00149', 'STD00150', 'STD00153', 'STD00155', 'STD00156', 'STD00159', 'STD00160', 'STD00161', 'STD00163', 'STD00165', 'STD00166', 'STD00169', 'STD00171', 'STD00173', 'STD00176', 'STD00178', 'STD00179', 'STD00180', 'STD00182', 'STD00183', 'STD00184', 'STD00188', 'STD00189', 'STD00192', 'STD00196', 'STD00197'] - -CS102 -['STD00001', 'STD00003', 'STD00005', 'STD00007', 'STD00010', 'STD00011', 'STD00012', 'STD00016', 'STD00019', 'STD00021', 'STD00024', 'STD00025', 'STD00026', 'STD00029', 'STD00032', 'STD00033', 'STD00036', 'STD00037', 'STD00039', 'STD00040', 'STD00041', 'STD00046', 'STD00047', 'STD00051', 'STD00054', 'STD00055', 'STD00058', 'STD00059', 'STD00060', 'STD00065', 'STD00066', 'STD00067', 'STD00072', 'STD00075', 'STD00076', 'STD00077', 'STD00078', 'STD00080', 'STD00081', 'STD00082', 'STD00085', 'STD00086', 'STD00088', 'STD00092', 'STD00094', 'STD00095', 'STD00098', 'STD00099', 'STD00100', 'STD00101', 'STD00112', 'STD00115', 'STD00120', 'STD00121', 'STD00123', 'STD00124', 'STD00127', 'STD00128', 'STD00132', 'STD00133', 'STD00135', 'STD00136', 'STD00138', 'STD00140', 'STD00142', 'STD00145', 'STD00148', 'STD00150', 'STD00152', 'STD00153', 'STD00155', 'STD00156', 'STD00157', 'STD00160', 'STD00161', 'STD00164', 'STD00165', 'STD00168', 'STD00169', 'STD00175', 'STD00176', 'STD00182', 'STD00184', 'STD00188', 'STD00189', 'STD00190', 'STD00191', 'STD00192', 'STD00197', 'STD00200'] - -CS103 -['STD00006', 'STD00009', 'STD00011', 'STD00014', 'STD00015', 'STD00017', 'STD00019', 'STD00020', 'STD00021', 'STD00022', 'STD00026', 'STD00027', 'STD00029', 'STD00030', 'STD00031', 'STD00036', 'STD00038', 'STD00039', 'STD00040', 'STD00041', 'STD00045', 'STD00046', 'STD00048', 'STD00050', 'STD00056', 'STD00060', 'STD00061', 'STD00062', 'STD00066', 'STD00067', 'STD00071', 'STD00077', 'STD00079', 'STD00080', 'STD00082', 'STD00085', 'STD00087', 'STD00090', 'STD00096', 'STD00102', 'STD00103', 'STD00104', 'STD00107', 'STD00110', 'STD00112', 'STD00114', 'STD00117', 'STD00118', 'STD00120', 'STD00121', 'STD00123', 'STD00128', 'STD00131', 'STD00134', 'STD00135', 'STD00138', 'STD00140', 'STD00144', 'STD00145', 'STD00146', 'STD00147', 'STD00148', 'STD00149', 'STD00151', 'STD00153', 'STD00154', 'STD00157', 'STD00165', 'STD00166', 'STD00169', 'STD00174', 'STD00175', 'STD00177', 'STD00179', 'STD00190', 'STD00193', 'STD00194', 'STD00198', 'STD00200'] - -CS104 -['STD00001', 'STD00004', 'STD00005', 'STD00009', 'STD00020', 'STD00022', 'STD00023', 'STD00024', 'STD00025', 'STD00027', 'STD00028', 'STD00030', 'STD00034', 'STD00035', 'STD00036', 'STD00048', 'STD00052', 'STD00053', 'STD00056', 'STD00058', 'STD00060', 'STD00061', 'STD00062', 'STD00064', 'STD00065', 'STD00066', 'STD00069', 'STD00071', 'STD00073', 'STD00074', 'STD00076', 'STD00079', 'STD00082', 'STD00083', 'STD00084', 'STD00086', 'STD00088', 'STD00089', 'STD00090', 'STD00093', 'STD00095', 'STD00097', 'STD00098', 'STD00101', 'STD00106', 'STD00108', 'STD00109', 'STD00110', 'STD00111', 'STD00114', 'STD00117', 'STD00118', 'STD00119', 'STD00123', 'STD00127', 'STD00132', 'STD00134', 'STD00135', 'STD00136', 'STD00139', 'STD00140', 'STD00142', 'STD00143', 'STD00144', 'STD00145', 'STD00146', 'STD00150', 'STD00151', 'STD00152', 'STD00156', 'STD00157', 'STD00162', 'STD00163', 'STD00164', 'STD00166', 'STD00168', 'STD00175', 'STD00177', 'STD00178', 'STD00180', 'STD00181', 'STD00185', 'STD00186', 'STD00190', 'STD00193', 'STD00196', 'STD00198', 'STD00199', 'STD00200'] - -CS105 -['STD00002', 'STD00010', 'STD00011', 'STD00012', 'STD00014', 'STD00019', 'STD00022', 'STD00030', 'STD00031', 'STD00035', 'STD00038', 'STD00039', 'STD00042', 'STD00043', 'STD00044', 'STD00045', 'STD00049', 'STD00053', 'STD00055', 'STD00057', 'STD00059', 'STD00061', 'STD00064', 'STD00068', 'STD00070', 'STD00083', 'STD00093', 'STD00096', 'STD00104', 'STD00107', 'STD00122', 'STD00129', 'STD00131', 'STD00133', 'STD00137', 'STD00139', 'STD00146', 'STD00154', 'STD00156', 'STD00158', 'STD00167', 'STD00170', 'STD00171', 'STD00172', 'STD00173', 'STD00174', 'STD00175', 'STD00178', 'STD00180', 'STD00181', 'STD00183', 'STD00184', 'STD00185', 'STD00187', 'STD00191', 'STD00193', 'STD00194', 'STD00197', 'STD00198', 'STD00199'] - -CS106 -['STD00005', 'STD00008', 'STD00009', 'STD00010', 'STD00015', 'STD00018', 'STD00019', 'STD00020', 'STD00025', 'STD00026', 'STD00032', 'STD00033', 'STD00034', 'STD00037', 'STD00042', 'STD00043', 'STD00044', 'STD00047', 'STD00048', 'STD00052', 'STD00053', 'STD00056', 'STD00057', 'STD00058', 'STD00059', 'STD00062', 'STD00064', 'STD00067', 'STD00069', 'STD00070', 'STD00075', 'STD00076', 'STD00079', 'STD00080', 'STD00081', 'STD00083', 'STD00085', 'STD00089', 'STD00092', 'STD00093', 'STD00095', 'STD00096', 'STD00097', 'STD00102', 'STD00103', 'STD00106', 'STD00107', 'STD00108', 'STD00110', 'STD00116', 'STD00118', 'STD00121', 'STD00123', 'STD00124', 'STD00129', 'STD00130', 'STD00131', 'STD00133', 'STD00134', 'STD00137', 'STD00139', 'STD00141', 'STD00142', 'STD00143', 'STD00145', 'STD00147', 'STD00148', 'STD00149', 'STD00150', 'STD00151', 'STD00152', 'STD00154', 'STD00157', 'STD00158', 'STD00159', 'STD00160', 'STD00161', 'STD00162', 'STD00163', 'STD00164', 'STD00167', 'STD00168', 'STD00170', 'STD00172', 'STD00182', 'STD00185', 'STD00186', 'STD00187', 'STD00193', 'STD00194', 'STD00195', 'STD00196'] - -CS107 -['STD00001', 'STD00002', 'STD00003', 'STD00004', 'STD00006', 'STD00007', 'STD00013', 'STD00016', 'STD00017', 'STD00018', 'STD00024', 'STD00028', 'STD00030', 'STD00034', 'STD00035', 'STD00036', 'STD00038', 'STD00044', 'STD00045', 'STD00049', 'STD00051', 'STD00054', 'STD00056', 'STD00058', 'STD00066', 'STD00068', 'STD00071', 'STD00072', 'STD00074', 'STD00078', 'STD00084', 'STD00085', 'STD00087', 'STD00091', 'STD00092', 'STD00094', 'STD00095', 'STD00101', 'STD00105', 'STD00106', 'STD00110', 'STD00111', 'STD00112', 'STD00113', 'STD00116', 'STD00120', 'STD00122', 'STD00126', 'STD00128', 'STD00136', 'STD00137', 'STD00141', 'STD00144', 'STD00146', 'STD00151', 'STD00155', 'STD00158', 'STD00160', 'STD00161', 'STD00163', 'STD00164', 'STD00169', 'STD00170', 'STD00173', 'STD00176', 'STD00179', 'STD00181', 'STD00184', 'STD00194', 'STD00195', 'STD00199', 'STD00200'] - -CS108 -['STD00001', 'STD00003', 'STD00004', 'STD00006', 'STD00008', 'STD00011', 'STD00012', 'STD00013', 'STD00018', 'STD00022', 'STD00023', 'STD00033', 'STD00034', 'STD00035', 'STD00040', 'STD00041', 'STD00043', 'STD00044', 'STD00045', 'STD00050', 'STD00052', 'STD00054', 'STD00057', 'STD00063', 'STD00065', 'STD00067', 'STD00069', 'STD00070', 'STD00073', 'STD00075', 'STD00078', 'STD00079', 'STD00080', 'STD00088', 'STD00091', 'STD00094', 'STD00096', 'STD00097', 'STD00098', 'STD00099', 'STD00100', 'STD00103', 'STD00109', 'STD00111', 'STD00112', 'STD00113', 'STD00116', 'STD00117', 'STD00119', 'STD00120', 'STD00121', 'STD00125', 'STD00126', 'STD00127', 'STD00130', 'STD00135', 'STD00139', 'STD00141', 'STD00144', 'STD00147', 'STD00149', 'STD00153', 'STD00159', 'STD00162', 'STD00168', 'STD00170', 'STD00171', 'STD00174', 'STD00177', 'STD00178', 'STD00180', 'STD00183', 'STD00186', 'STD00187', 'STD00189', 'STD00191', 'STD00192', 'STD00195', 'STD00197'] - -CS109 -['STD00004', 'STD00006', 'STD00008', 'STD00012', 'STD00016', 'STD00017', 'STD00021', 'STD00023', 'STD00027', 'STD00029', 'STD00031', 'STD00032', 'STD00037', 'STD00038', 'STD00040', 'STD00042', 'STD00047', 'STD00059', 'STD00062', 'STD00063', 'STD00068', 'STD00072', 'STD00073', 'STD00074', 'STD00081', 'STD00082', 'STD00086', 'STD00087', 'STD00091', 'STD00097', 'STD00099', 'STD00100', 'STD00101', 'STD00104', 'STD00105', 'STD00109', 'STD00114', 'STD00115', 'STD00119', 'STD00122', 'STD00124', 'STD00125', 'STD00126', 'STD00129', 'STD00130', 'STD00131', 'STD00134', 'STD00137', 'STD00141', 'STD00142', 'STD00147', 'STD00148', 'STD00152', 'STD00159', 'STD00162', 'STD00166', 'STD00167', 'STD00172', 'STD00177', 'STD00179', 'STD00182', 'STD00183', 'STD00185', 'STD00186', 'STD00188', 'STD00199'] - -CS110 -['STD00002', 'STD00005', 'STD00007', 'STD00009', 'STD00010', 'STD00013', 'STD00014', 'STD00015', 'STD00017', 'STD00020', 'STD00021', 'STD00023', 'STD00027', 'STD00028', 'STD00033', 'STD00046', 'STD00048', 'STD00049', 'STD00050', 'STD00051', 'STD00053', 'STD00055', 'STD00057', 'STD00061', 'STD00063', 'STD00064', 'STD00068', 'STD00069', 'STD00071', 'STD00072', 'STD00077', 'STD00084', 'STD00089', 'STD00090', 'STD00099', 'STD00100', 'STD00102', 'STD00105', 'STD00108', 'STD00113', 'STD00114', 'STD00115', 'STD00117', 'STD00118', 'STD00125', 'STD00126', 'STD00127', 'STD00132', 'STD00138', 'STD00140', 'STD00143', 'STD00154', 'STD00155', 'STD00158', 'STD00165', 'STD00167', 'STD00171', 'STD00172', 'STD00173', 'STD00174', 'STD00176', 'STD00181', 'STD00187', 'STD00188', 'STD00189', 'STD00190', 'STD00191', 'STD00192', 'STD00195', 'STD00196', 'STD00198'] - -MATH101 -['STD00201', 'STD00202', 'STD00206', 'STD00207', 'STD00211', 'STD00214', 'STD00215', 'STD00216', 'STD00220', 'STD00221', 'STD00222', 'STD00224', 'STD00225', 'STD00226', 'STD00227', 'STD00228', 'STD00230', 'STD00233', 'STD00235', 'STD00236', 'STD00237', 'STD00240', 'STD00244', 'STD00248', 'STD00249', 'STD00251', 'STD00256', 'STD00258', 'STD00261', 'STD00262', 'STD00264', 'STD00266', 'STD00267', 'STD00271', 'STD00273', 'STD00275', 'STD00281', 'STD00284', 'STD00288', 'STD00289', 'STD00290', 'STD00293', 'STD00294', 'STD00296', 'STD00298', 'STD00302', 'STD00304', 'STD00305', 'STD00306', 'STD00307', 'STD00309', 'STD00311', 'STD00316', 'STD00317', 'STD00318', 'STD00321', 'STD00322', 'STD00323', 'STD00326', 'STD00327', 'STD00328', 'STD00330', 'STD00331', 'STD00332', 'STD00335', 'STD00337', 'STD00338', 'STD00339', 'STD00345', 'STD00346', 'STD00347', 'STD00349', 'STD00350', 'STD00351', 'STD00353', 'STD00355', 'STD00362', 'STD00363', 'STD00364', 'STD00366', 'STD00372', 'STD00374', 'STD00376', 'STD00377', 'STD00378', 'STD00382', 'STD00385', 'STD00387', 'STD00388', 'STD00389', 'STD00391', 'STD00392', 'STD00397', 'STD00398'] - -MATH102 -['STD00201', 'STD00202', 'STD00203', 'STD00204', 'STD00206', 'STD00209', 'STD00210', 'STD00211', 'STD00212', 'STD00214', 'STD00218', 'STD00219', 'STD00220', 'STD00223', 'STD00225', 'STD00228', 'STD00233', 'STD00234', 'STD00235', 'STD00236', 'STD00237', 'STD00238', 'STD00239', 'STD00240', 'STD00243', 'STD00245', 'STD00247', 'STD00248', 'STD00249', 'STD00255', 'STD00256', 'STD00257', 'STD00259', 'STD00261', 'STD00264', 'STD00265', 'STD00267', 'STD00272', 'STD00280', 'STD00281', 'STD00282', 'STD00284', 'STD00285', 'STD00288', 'STD00291', 'STD00292', 'STD00293', 'STD00296', 'STD00301', 'STD00303', 'STD00304', 'STD00305', 'STD00307', 'STD00309', 'STD00311', 'STD00315', 'STD00316', 'STD00318', 'STD00321', 'STD00323', 'STD00325', 'STD00326', 'STD00328', 'STD00329', 'STD00332', 'STD00333', 'STD00335', 'STD00336', 'STD00337', 'STD00338', 'STD00343', 'STD00345', 'STD00346', 'STD00347', 'STD00351', 'STD00354', 'STD00357', 'STD00359', 'STD00360', 'STD00361', 'STD00362', 'STD00365', 'STD00366', 'STD00367', 'STD00369', 'STD00370', 'STD00371', 'STD00373', 'STD00374', 'STD00375', 'STD00376', 'STD00382', 'STD00384', 'STD00385', 'STD00386', 'STD00387', 'STD00389', 'STD00391', 'STD00392', 'STD00395', 'STD00400'] - -MATH103 -['STD00204', 'STD00205', 'STD00209', 'STD00211', 'STD00212', 'STD00216', 'STD00218', 'STD00228', 'STD00230', 'STD00231', 'STD00233', 'STD00234', 'STD00235', 'STD00238', 'STD00242', 'STD00247', 'STD00253', 'STD00255', 'STD00259', 'STD00265', 'STD00267', 'STD00272', 'STD00274', 'STD00279', 'STD00285', 'STD00287', 'STD00288', 'STD00291', 'STD00293', 'STD00295', 'STD00299', 'STD00300', 'STD00303', 'STD00308', 'STD00312', 'STD00321', 'STD00324', 'STD00328', 'STD00330', 'STD00332', 'STD00333', 'STD00334', 'STD00336', 'STD00340', 'STD00341', 'STD00347', 'STD00350', 'STD00352', 'STD00353', 'STD00354', 'STD00356', 'STD00357', 'STD00361', 'STD00363', 'STD00367', 'STD00368', 'STD00370', 'STD00371', 'STD00374', 'STD00379', 'STD00380', 'STD00381', 'STD00383', 'STD00384', 'STD00389', 'STD00395', 'STD00398', 'STD00400'] - -MATH104 -['STD00203', 'STD00205', 'STD00208', 'STD00210', 'STD00212', 'STD00213', 'STD00215', 'STD00219', 'STD00221', 'STD00224', 'STD00226', 'STD00227', 'STD00231', 'STD00232', 'STD00234', 'STD00237', 'STD00242', 'STD00243', 'STD00244', 'STD00245', 'STD00246', 'STD00247', 'STD00249', 'STD00250', 'STD00251', 'STD00255', 'STD00260', 'STD00263', 'STD00264', 'STD00266', 'STD00269', 'STD00270', 'STD00271', 'STD00274', 'STD00276', 'STD00278', 'STD00279', 'STD00280', 'STD00281', 'STD00282', 'STD00283', 'STD00287', 'STD00288', 'STD00290', 'STD00296', 'STD00298', 'STD00299', 'STD00300', 'STD00301', 'STD00302', 'STD00305', 'STD00310', 'STD00316', 'STD00317', 'STD00320', 'STD00329', 'STD00330', 'STD00331', 'STD00333', 'STD00335', 'STD00336', 'STD00337', 'STD00340', 'STD00345', 'STD00346', 'STD00348', 'STD00349', 'STD00352', 'STD00354', 'STD00355', 'STD00356', 'STD00358', 'STD00361', 'STD00366', 'STD00367', 'STD00373', 'STD00375', 'STD00377', 'STD00378', 'STD00380', 'STD00381', 'STD00383', 'STD00386', 'STD00390', 'STD00393', 'STD00395', 'STD00396', 'STD00398', 'STD00399'] - -MATH105 -['STD00203', 'STD00207', 'STD00213', 'STD00217', 'STD00220', 'STD00232', 'STD00233', 'STD00239', 'STD00246', 'STD00252', 'STD00253', 'STD00254', 'STD00255', 'STD00260', 'STD00263', 'STD00268', 'STD00272', 'STD00275', 'STD00276', 'STD00277', 'STD00281', 'STD00286', 'STD00293', 'STD00308', 'STD00313', 'STD00314', 'STD00315', 'STD00316', 'STD00319', 'STD00320', 'STD00340', 'STD00342', 'STD00343', 'STD00344', 'STD00348', 'STD00350', 'STD00352', 'STD00358', 'STD00359', 'STD00364', 'STD00365', 'STD00381', 'STD00382', 'STD00383', 'STD00388', 'STD00390', 'STD00391', 'STD00397', 'STD00399'] - -MATH106 -['STD00201', 'STD00204', 'STD00208', 'STD00209', 'STD00211', 'STD00213', 'STD00215', 'STD00218', 'STD00224', 'STD00226', 'STD00227', 'STD00234', 'STD00236', 'STD00238', 'STD00241', 'STD00242', 'STD00243', 'STD00244', 'STD00245', 'STD00246', 'STD00249', 'STD00250', 'STD00251', 'STD00252', 'STD00256', 'STD00258', 'STD00259', 'STD00262', 'STD00263', 'STD00265', 'STD00266', 'STD00269', 'STD00270', 'STD00271', 'STD00273', 'STD00278', 'STD00283', 'STD00286', 'STD00294', 'STD00295', 'STD00297', 'STD00300', 'STD00306', 'STD00311', 'STD00313', 'STD00314', 'STD00317', 'STD00319', 'STD00320', 'STD00323', 'STD00324', 'STD00325', 'STD00329', 'STD00331', 'STD00332', 'STD00334', 'STD00337', 'STD00338', 'STD00340', 'STD00342', 'STD00343', 'STD00345', 'STD00347', 'STD00351', 'STD00354', 'STD00356', 'STD00357', 'STD00358', 'STD00363', 'STD00364', 'STD00365', 'STD00366', 'STD00368', 'STD00369', 'STD00370', 'STD00372', 'STD00373', 'STD00376', 'STD00379', 'STD00386', 'STD00389', 'STD00390', 'STD00392', 'STD00393', 'STD00394', 'STD00397', 'STD00399', 'STD00400'] - -MATH107 -['STD00201', 'STD00202', 'STD00204', 'STD00206', 'STD00213', 'STD00214', 'STD00217', 'STD00222', 'STD00223', 'STD00224', 'STD00225', 'STD00227', 'STD00229', 'STD00232', 'STD00238', 'STD00239', 'STD00240', 'STD00241', 'STD00248', 'STD00253', 'STD00254', 'STD00257', 'STD00261', 'STD00266', 'STD00268', 'STD00269', 'STD00274', 'STD00276', 'STD00277', 'STD00279', 'STD00282', 'STD00286', 'STD00290', 'STD00291', 'STD00292', 'STD00294', 'STD00297', 'STD00303', 'STD00305', 'STD00307', 'STD00323', 'STD00324', 'STD00326', 'STD00330', 'STD00339', 'STD00341', 'STD00342', 'STD00346', 'STD00348', 'STD00351', 'STD00355', 'STD00358', 'STD00359', 'STD00360', 'STD00361', 'STD00365', 'STD00369', 'STD00371', 'STD00372', 'STD00374', 'STD00375', 'STD00378', 'STD00380', 'STD00384', 'STD00386', 'STD00387', 'STD00393', 'STD00394', 'STD00396', 'STD00398'] - -MATH108 -['STD00206', 'STD00210', 'STD00212', 'STD00214', 'STD00217', 'STD00218', 'STD00222', 'STD00223', 'STD00229', 'STD00230', 'STD00245', 'STD00247', 'STD00250', 'STD00251', 'STD00252', 'STD00254', 'STD00256', 'STD00258', 'STD00260', 'STD00261', 'STD00262', 'STD00264', 'STD00265', 'STD00268', 'STD00269', 'STD00270', 'STD00271', 'STD00273', 'STD00274', 'STD00276', 'STD00277', 'STD00278', 'STD00280', 'STD00285', 'STD00287', 'STD00289', 'STD00290', 'STD00291', 'STD00292', 'STD00297', 'STD00298', 'STD00299', 'STD00301', 'STD00302', 'STD00304', 'STD00307', 'STD00310', 'STD00312', 'STD00313', 'STD00314', 'STD00315', 'STD00318', 'STD00319', 'STD00320', 'STD00321', 'STD00322', 'STD00325', 'STD00326', 'STD00327', 'STD00334', 'STD00339', 'STD00342', 'STD00343', 'STD00344', 'STD00348', 'STD00349', 'STD00352', 'STD00353', 'STD00357', 'STD00360', 'STD00362', 'STD00364', 'STD00367', 'STD00368', 'STD00371', 'STD00372', 'STD00373', 'STD00377', 'STD00378', 'STD00379', 'STD00380', 'STD00381', 'STD00382', 'STD00383', 'STD00384', 'STD00385', 'STD00387', 'STD00388', 'STD00390', 'STD00392', 'STD00393', 'STD00394', 'STD00399'] - -MATH109 -['STD00205', 'STD00207', 'STD00208', 'STD00209', 'STD00215', 'STD00216', 'STD00219', 'STD00221', 'STD00222', 'STD00225', 'STD00226', 'STD00229', 'STD00230', 'STD00231', 'STD00232', 'STD00235', 'STD00236', 'STD00241', 'STD00246', 'STD00253', 'STD00254', 'STD00257', 'STD00260', 'STD00267', 'STD00272', 'STD00273', 'STD00275', 'STD00277', 'STD00278', 'STD00282', 'STD00283', 'STD00284', 'STD00289', 'STD00294', 'STD00295', 'STD00296', 'STD00298', 'STD00306', 'STD00308', 'STD00309', 'STD00310', 'STD00312', 'STD00313', 'STD00314', 'STD00322', 'STD00327', 'STD00328', 'STD00334', 'STD00338', 'STD00339', 'STD00341', 'STD00344', 'STD00349', 'STD00350', 'STD00353', 'STD00362', 'STD00377', 'STD00385', 'STD00394', 'STD00395', 'STD00396', 'STD00397', 'STD00400'] - -MATH110 -['STD00202', 'STD00203', 'STD00205', 'STD00207', 'STD00208', 'STD00210', 'STD00216', 'STD00217', 'STD00219', 'STD00220', 'STD00221', 'STD00223', 'STD00228', 'STD00229', 'STD00231', 'STD00237', 'STD00239', 'STD00240', 'STD00241', 'STD00242', 'STD00243', 'STD00244', 'STD00248', 'STD00250', 'STD00252', 'STD00257', 'STD00258', 'STD00259', 'STD00262', 'STD00263', 'STD00268', 'STD00270', 'STD00275', 'STD00279', 'STD00280', 'STD00283', 'STD00284', 'STD00285', 'STD00286', 'STD00287', 'STD00289', 'STD00292', 'STD00295', 'STD00297', 'STD00299', 'STD00300', 'STD00301', 'STD00302', 'STD00303', 'STD00304', 'STD00306', 'STD00308', 'STD00309', 'STD00310', 'STD00311', 'STD00312', 'STD00315', 'STD00317', 'STD00318', 'STD00319', 'STD00322', 'STD00324', 'STD00325', 'STD00327', 'STD00329', 'STD00331', 'STD00333', 'STD00335', 'STD00336', 'STD00341', 'STD00344', 'STD00355', 'STD00356', 'STD00359', 'STD00360', 'STD00363', 'STD00368', 'STD00369', 'STD00370', 'STD00375', 'STD00376', 'STD00379', 'STD00388', 'STD00391', 'STD00396'] - -PHYS101 -['STD00401', 'STD00403', 'STD00404', 'STD00406', 'STD00408', 'STD00409', 'STD00410', 'STD00411', 'STD00412', 'STD00416', 'STD00417', 'STD00419', 'STD00422', 'STD00424', 'STD00425', 'STD00426', 'STD00428', 'STD00433', 'STD00435', 'STD00436', 'STD00437', 'STD00443', 'STD00445', 'STD00447', 'STD00449', 'STD00450', 'STD00452', 'STD00454', 'STD00463', 'STD00464', 'STD00469', 'STD00471', 'STD00474', 'STD00476', 'STD00477', 'STD00478', 'STD00481', 'STD00482', 'STD00484', 'STD00486', 'STD00488', 'STD00491', 'STD00492', 'STD00494', 'STD00495', 'STD00497', 'STD00498', 'STD00499', 'STD00501', 'STD00502', 'STD00503', 'STD00505', 'STD00506', 'STD00508', 'STD00509', 'STD00512', 'STD00513', 'STD00515', 'STD00516', 'STD00517', 'STD00524', 'STD00525', 'STD00526', 'STD00534', 'STD00537', 'STD00538', 'STD00539', 'STD00542', 'STD00545', 'STD00546', 'STD00549', 'STD00550', 'STD00551', 'STD00552', 'STD00553', 'STD00556', 'STD00558', 'STD00559', 'STD00560', 'STD00562', 'STD00567', 'STD00569', 'STD00570', 'STD00571', 'STD00572', 'STD00573', 'STD00574', 'STD00575', 'STD00577', 'STD00581', 'STD00583', 'STD00584', 'STD00586', 'STD00589', 'STD00591', 'STD00594'] - -PHYS102 -['STD00401', 'STD00407', 'STD00408', 'STD00409', 'STD00415', 'STD00421', 'STD00425', 'STD00427', 'STD00431', 'STD00432', 'STD00433', 'STD00436', 'STD00439', 'STD00440', 'STD00441', 'STD00442', 'STD00447', 'STD00448', 'STD00449', 'STD00450', 'STD00453', 'STD00454', 'STD00455', 'STD00458', 'STD00461', 'STD00463', 'STD00464', 'STD00466', 'STD00467', 'STD00468', 'STD00469', 'STD00470', 'STD00471', 'STD00472', 'STD00473', 'STD00474', 'STD00478', 'STD00479', 'STD00480', 'STD00482', 'STD00483', 'STD00484', 'STD00486', 'STD00488', 'STD00491', 'STD00495', 'STD00496', 'STD00497', 'STD00498', 'STD00499', 'STD00502', 'STD00504', 'STD00505', 'STD00506', 'STD00509', 'STD00511', 'STD00515', 'STD00517', 'STD00518', 'STD00519', 'STD00522', 'STD00524', 'STD00525', 'STD00526', 'STD00530', 'STD00533', 'STD00534', 'STD00535', 'STD00537', 'STD00538', 'STD00540', 'STD00541', 'STD00542', 'STD00543', 'STD00546', 'STD00549', 'STD00550', 'STD00551', 'STD00554', 'STD00556', 'STD00558', 'STD00560', 'STD00566', 'STD00567', 'STD00569', 'STD00570', 'STD00571', 'STD00574', 'STD00575', 'STD00577', 'STD00582', 'STD00583', 'STD00584', 'STD00586', 'STD00588', 'STD00589', 'STD00590', 'STD00592', 'STD00594', 'STD00595', 'STD00597', 'STD00598', 'STD00600'] - -PHYS103 -['STD00402', 'STD00404', 'STD00410', 'STD00411', 'STD00414', 'STD00415', 'STD00416', 'STD00421', 'STD00423', 'STD00426', 'STD00429', 'STD00430', 'STD00431', 'STD00432', 'STD00433', 'STD00434', 'STD00436', 'STD00437', 'STD00438', 'STD00440', 'STD00442', 'STD00443', 'STD00444', 'STD00451', 'STD00453', 'STD00455', 'STD00458', 'STD00462', 'STD00465', 'STD00466', 'STD00471', 'STD00475', 'STD00480', 'STD00482', 'STD00483', 'STD00484', 'STD00485', 'STD00487', 'STD00489', 'STD00493', 'STD00498', 'STD00500', 'STD00502', 'STD00503', 'STD00505', 'STD00506', 'STD00507', 'STD00510', 'STD00514', 'STD00519', 'STD00520', 'STD00521', 'STD00527', 'STD00528', 'STD00530', 'STD00535', 'STD00536', 'STD00542', 'STD00543', 'STD00547', 'STD00549', 'STD00552', 'STD00554', 'STD00556', 'STD00558', 'STD00559', 'STD00564', 'STD00565', 'STD00566', 'STD00569', 'STD00577', 'STD00578', 'STD00580', 'STD00583', 'STD00584', 'STD00585', 'STD00589', 'STD00595', 'STD00596', 'STD00597', 'STD00598', 'STD00600'] - -PHYS104 -['STD00402', 'STD00406', 'STD00407', 'STD00409', 'STD00410', 'STD00413', 'STD00418', 'STD00419', 'STD00420', 'STD00421', 'STD00429', 'STD00431', 'STD00432', 'STD00438', 'STD00442', 'STD00443', 'STD00444', 'STD00445', 'STD00447', 'STD00448', 'STD00451', 'STD00453', 'STD00454', 'STD00455', 'STD00460', 'STD00461', 'STD00462', 'STD00465', 'STD00469', 'STD00470', 'STD00473', 'STD00477', 'STD00479', 'STD00481', 'STD00482', 'STD00487', 'STD00492', 'STD00493', 'STD00494', 'STD00496', 'STD00497', 'STD00500', 'STD00501', 'STD00504', 'STD00505', 'STD00508', 'STD00511', 'STD00512', 'STD00513', 'STD00514', 'STD00517', 'STD00520', 'STD00521', 'STD00522', 'STD00523', 'STD00527', 'STD00532', 'STD00533', 'STD00534', 'STD00541', 'STD00545', 'STD00547', 'STD00548', 'STD00553', 'STD00557', 'STD00558', 'STD00562', 'STD00563', 'STD00564', 'STD00571', 'STD00572', 'STD00573', 'STD00576', 'STD00579', 'STD00580', 'STD00582', 'STD00587', 'STD00589', 'STD00590', 'STD00593', 'STD00594', 'STD00595', 'STD00597', 'STD00599'] - -PHYS105 -['STD00402', 'STD00403', 'STD00406', 'STD00407', 'STD00408', 'STD00411', 'STD00412', 'STD00414', 'STD00415', 'STD00417', 'STD00418', 'STD00422', 'STD00426', 'STD00427', 'STD00428', 'STD00429', 'STD00434', 'STD00435', 'STD00438', 'STD00439', 'STD00442', 'STD00446', 'STD00451', 'STD00456', 'STD00457', 'STD00460', 'STD00465', 'STD00472', 'STD00477', 'STD00483', 'STD00484', 'STD00485', 'STD00487', 'STD00490', 'STD00492', 'STD00493', 'STD00497', 'STD00500', 'STD00503', 'STD00507', 'STD00508', 'STD00510', 'STD00511', 'STD00516', 'STD00518', 'STD00522', 'STD00523', 'STD00526', 'STD00527', 'STD00531', 'STD00532', 'STD00536', 'STD00544', 'STD00545', 'STD00555', 'STD00556', 'STD00557', 'STD00561', 'STD00562', 'STD00563', 'STD00564', 'STD00565', 'STD00569', 'STD00576', 'STD00579', 'STD00585', 'STD00587', 'STD00591', 'STD00593', 'STD00598', 'STD00599'] - -PHYS106 -['STD00403', 'STD00404', 'STD00405', 'STD00406', 'STD00420', 'STD00422', 'STD00424', 'STD00427', 'STD00434', 'STD00435', 'STD00438', 'STD00443', 'STD00444', 'STD00446', 'STD00450', 'STD00452', 'STD00454', 'STD00456', 'STD00457', 'STD00458', 'STD00459', 'STD00460', 'STD00461', 'STD00462', 'STD00465', 'STD00466', 'STD00472', 'STD00474', 'STD00477', 'STD00480', 'STD00481', 'STD00491', 'STD00493', 'STD00499', 'STD00502', 'STD00504', 'STD00510', 'STD00513', 'STD00514', 'STD00515', 'STD00516', 'STD00517', 'STD00519', 'STD00524', 'STD00526', 'STD00528', 'STD00530', 'STD00531', 'STD00532', 'STD00539', 'STD00544', 'STD00546', 'STD00547', 'STD00548', 'STD00551', 'STD00552', 'STD00554', 'STD00555', 'STD00557', 'STD00560', 'STD00561', 'STD00563', 'STD00564', 'STD00568', 'STD00572', 'STD00573', 'STD00575', 'STD00576', 'STD00578', 'STD00579', 'STD00580', 'STD00587', 'STD00592', 'STD00599'] - -PHYS107 -['STD00405', 'STD00407', 'STD00410', 'STD00412', 'STD00413', 'STD00414', 'STD00417', 'STD00418', 'STD00423', 'STD00425', 'STD00428', 'STD00429', 'STD00430', 'STD00436', 'STD00437', 'STD00439', 'STD00441', 'STD00444', 'STD00451', 'STD00452', 'STD00456', 'STD00457', 'STD00459', 'STD00461', 'STD00463', 'STD00464', 'STD00467', 'STD00468', 'STD00475', 'STD00476', 'STD00479', 'STD00487', 'STD00488', 'STD00489', 'STD00490', 'STD00495', 'STD00496', 'STD00507', 'STD00512', 'STD00513', 'STD00520', 'STD00522', 'STD00527', 'STD00529', 'STD00531', 'STD00533', 'STD00540', 'STD00543', 'STD00547', 'STD00560', 'STD00562', 'STD00567', 'STD00568', 'STD00570', 'STD00582', 'STD00585', 'STD00591', 'STD00592', 'STD00593', 'STD00595', 'STD00600'] - -PHYS108 -['STD00401', 'STD00402', 'STD00403', 'STD00404', 'STD00405', 'STD00408', 'STD00415', 'STD00416', 'STD00418', 'STD00419', 'STD00420', 'STD00421', 'STD00422', 'STD00424', 'STD00426', 'STD00430', 'STD00434', 'STD00441', 'STD00445', 'STD00446', 'STD00448', 'STD00449', 'STD00450', 'STD00453', 'STD00455', 'STD00457', 'STD00458', 'STD00459', 'STD00460', 'STD00466', 'STD00467', 'STD00468', 'STD00469', 'STD00475', 'STD00480', 'STD00481', 'STD00485', 'STD00486', 'STD00489', 'STD00490', 'STD00491', 'STD00495', 'STD00496', 'STD00498', 'STD00499', 'STD00500', 'STD00501', 'STD00504', 'STD00506', 'STD00507', 'STD00508', 'STD00509', 'STD00511', 'STD00512', 'STD00514', 'STD00515', 'STD00516', 'STD00518', 'STD00520', 'STD00521', 'STD00523', 'STD00524', 'STD00525', 'STD00528', 'STD00529', 'STD00530', 'STD00531', 'STD00532', 'STD00533', 'STD00535', 'STD00536', 'STD00537', 'STD00539', 'STD00540', 'STD00542', 'STD00543', 'STD00544', 'STD00546', 'STD00551', 'STD00552', 'STD00555', 'STD00559', 'STD00561', 'STD00565', 'STD00567', 'STD00568', 'STD00570', 'STD00571', 'STD00575', 'STD00577', 'STD00580', 'STD00581', 'STD00582', 'STD00584', 'STD00586', 'STD00588', 'STD00594', 'STD00596', 'STD00600'] - -PHYS109 -['STD00405', 'STD00409', 'STD00413', 'STD00417', 'STD00423', 'STD00424', 'STD00427', 'STD00428', 'STD00431', 'STD00432', 'STD00433', 'STD00435', 'STD00437', 'STD00439', 'STD00440', 'STD00445', 'STD00447', 'STD00448', 'STD00449', 'STD00452', 'STD00456', 'STD00459', 'STD00463', 'STD00467', 'STD00468', 'STD00470', 'STD00471', 'STD00472', 'STD00473', 'STD00475', 'STD00476', 'STD00478', 'STD00485', 'STD00490', 'STD00494', 'STD00501', 'STD00503', 'STD00510', 'STD00521', 'STD00528', 'STD00529', 'STD00536', 'STD00537', 'STD00538', 'STD00540', 'STD00541', 'STD00544', 'STD00545', 'STD00548', 'STD00549', 'STD00550', 'STD00553', 'STD00557', 'STD00566', 'STD00568', 'STD00572', 'STD00574', 'STD00578', 'STD00581', 'STD00583', 'STD00586', 'STD00587', 'STD00588', 'STD00590', 'STD00592', 'STD00596', 'STD00598', 'STD00599'] - -PHYS110 -['STD00401', 'STD00411', 'STD00412', 'STD00413', 'STD00414', 'STD00416', 'STD00419', 'STD00420', 'STD00423', 'STD00425', 'STD00430', 'STD00440', 'STD00441', 'STD00446', 'STD00462', 'STD00464', 'STD00470', 'STD00473', 'STD00474', 'STD00476', 'STD00478', 'STD00479', 'STD00483', 'STD00486', 'STD00488', 'STD00489', 'STD00492', 'STD00494', 'STD00509', 'STD00518', 'STD00519', 'STD00523', 'STD00525', 'STD00529', 'STD00534', 'STD00535', 'STD00538', 'STD00539', 'STD00541', 'STD00548', 'STD00550', 'STD00553', 'STD00554', 'STD00555', 'STD00559', 'STD00561', 'STD00563', 'STD00565', 'STD00566', 'STD00573', 'STD00574', 'STD00576', 'STD00578', 'STD00579', 'STD00581', 'STD00585', 'STD00588', 'STD00590', 'STD00591', 'STD00593', 'STD00596', 'STD00597'] - -CHEM101 -['STD00605', 'STD00607', 'STD00611', 'STD00613', 'STD00615', 'STD00616', 'STD00620', 'STD00628', 'STD00629', 'STD00630', 'STD00631', 'STD00636', 'STD00638', 'STD00644', 'STD00649', 'STD00651', 'STD00652', 'STD00653', 'STD00654', 'STD00655', 'STD00656', 'STD00657', 'STD00659', 'STD00661', 'STD00663', 'STD00664', 'STD00666', 'STD00669', 'STD00672', 'STD00673', 'STD00674', 'STD00675', 'STD00676', 'STD00678', 'STD00682', 'STD00683', 'STD00684', 'STD00685', 'STD00686', 'STD00687', 'STD00688', 'STD00693', 'STD00699', 'STD00703', 'STD00704', 'STD00710', 'STD00711', 'STD00712', 'STD00714', 'STD00719', 'STD00722', 'STD00723', 'STD00725', 'STD00727', 'STD00732', 'STD00734', 'STD00736', 'STD00738', 'STD00740', 'STD00741', 'STD00742', 'STD00744', 'STD00745', 'STD00747', 'STD00749', 'STD00750', 'STD00754', 'STD00755', 'STD00763', 'STD00767', 'STD00770', 'STD00771', 'STD00772', 'STD00773', 'STD00776', 'STD00777', 'STD00778', 'STD00780', 'STD00782', 'STD00783', 'STD00786', 'STD00789', 'STD00791', 'STD00794', 'STD00795', 'STD00797', 'STD00799', 'STD00800'] - -CHEM102 -['STD00605', 'STD00607', 'STD00609', 'STD00613', 'STD00614', 'STD00617', 'STD00619', 'STD00620', 'STD00621', 'STD00622', 'STD00625', 'STD00627', 'STD00629', 'STD00631', 'STD00632', 'STD00633', 'STD00635', 'STD00637', 'STD00640', 'STD00648', 'STD00651', 'STD00652', 'STD00653', 'STD00655', 'STD00657', 'STD00660', 'STD00661', 'STD00663', 'STD00664', 'STD00665', 'STD00669', 'STD00672', 'STD00673', 'STD00675', 'STD00680', 'STD00682', 'STD00683', 'STD00684', 'STD00687', 'STD00690', 'STD00691', 'STD00693', 'STD00695', 'STD00697', 'STD00698', 'STD00699', 'STD00700', 'STD00703', 'STD00704', 'STD00706', 'STD00710', 'STD00711', 'STD00713', 'STD00714', 'STD00717', 'STD00718', 'STD00719', 'STD00721', 'STD00722', 'STD00723', 'STD00724', 'STD00725', 'STD00728', 'STD00729', 'STD00731', 'STD00732', 'STD00734', 'STD00740', 'STD00741', 'STD00742', 'STD00744', 'STD00749', 'STD00753', 'STD00754', 'STD00755', 'STD00757', 'STD00759', 'STD00761', 'STD00763', 'STD00766', 'STD00767', 'STD00769', 'STD00770', 'STD00771', 'STD00772', 'STD00777', 'STD00778', 'STD00780', 'STD00783', 'STD00784', 'STD00785', 'STD00791', 'STD00792', 'STD00794', 'STD00797', 'STD00799', 'STD00800'] - -CHEM103 -['STD00603', 'STD00606', 'STD00611', 'STD00617', 'STD00618', 'STD00623', 'STD00625', 'STD00626', 'STD00629', 'STD00630', 'STD00631', 'STD00632', 'STD00634', 'STD00635', 'STD00636', 'STD00641', 'STD00643', 'STD00644', 'STD00647', 'STD00648', 'STD00649', 'STD00655', 'STD00658', 'STD00659', 'STD00660', 'STD00662', 'STD00668', 'STD00669', 'STD00670', 'STD00674', 'STD00679', 'STD00680', 'STD00681', 'STD00683', 'STD00691', 'STD00694', 'STD00695', 'STD00696', 'STD00697', 'STD00698', 'STD00700', 'STD00702', 'STD00705', 'STD00706', 'STD00709', 'STD00713', 'STD00714', 'STD00715', 'STD00716', 'STD00718', 'STD00720', 'STD00722', 'STD00723', 'STD00724', 'STD00726', 'STD00728', 'STD00729', 'STD00733', 'STD00735', 'STD00737', 'STD00744', 'STD00751', 'STD00752', 'STD00754', 'STD00757', 'STD00758', 'STD00761', 'STD00764', 'STD00766', 'STD00768', 'STD00771', 'STD00774', 'STD00783', 'STD00784', 'STD00788', 'STD00791', 'STD00792', 'STD00793', 'STD00795', 'STD00796'] - -CHEM104 -['STD00603', 'STD00604', 'STD00606', 'STD00607', 'STD00609', 'STD00610', 'STD00612', 'STD00613', 'STD00614', 'STD00615', 'STD00617', 'STD00618', 'STD00621', 'STD00622', 'STD00623', 'STD00626', 'STD00628', 'STD00629', 'STD00630', 'STD00633', 'STD00634', 'STD00635', 'STD00640', 'STD00641', 'STD00642', 'STD00643', 'STD00644', 'STD00645', 'STD00646', 'STD00650', 'STD00652', 'STD00653', 'STD00654', 'STD00662', 'STD00665', 'STD00669', 'STD00670', 'STD00671', 'STD00677', 'STD00680', 'STD00681', 'STD00682', 'STD00689', 'STD00691', 'STD00692', 'STD00694', 'STD00697', 'STD00700', 'STD00701', 'STD00702', 'STD00705', 'STD00715', 'STD00717', 'STD00726', 'STD00727', 'STD00728', 'STD00729', 'STD00730', 'STD00733', 'STD00735', 'STD00737', 'STD00738', 'STD00739', 'STD00743', 'STD00746', 'STD00748', 'STD00751', 'STD00752', 'STD00753', 'STD00754', 'STD00759', 'STD00760', 'STD00761', 'STD00762', 'STD00764', 'STD00765', 'STD00766', 'STD00767', 'STD00768', 'STD00773', 'STD00774', 'STD00775', 'STD00779', 'STD00782', 'STD00784', 'STD00785', 'STD00789', 'STD00790', 'STD00791', 'STD00792', 'STD00795', 'STD00796', 'STD00797', 'STD00798'] - -CHEM105 -['STD00602', 'STD00606', 'STD00607', 'STD00612', 'STD00613', 'STD00615', 'STD00616', 'STD00620', 'STD00621', 'STD00624', 'STD00625', 'STD00626', 'STD00632', 'STD00633', 'STD00634', 'STD00639', 'STD00640', 'STD00641', 'STD00643', 'STD00646', 'STD00656', 'STD00657', 'STD00658', 'STD00659', 'STD00668', 'STD00671', 'STD00677', 'STD00678', 'STD00688', 'STD00689', 'STD00694', 'STD00697', 'STD00707', 'STD00709', 'STD00710', 'STD00712', 'STD00716', 'STD00719', 'STD00720', 'STD00721', 'STD00725', 'STD00739', 'STD00740', 'STD00743', 'STD00748', 'STD00756', 'STD00757', 'STD00762', 'STD00763', 'STD00765', 'STD00767', 'STD00772', 'STD00774', 'STD00775', 'STD00776', 'STD00781', 'STD00784', 'STD00785', 'STD00787', 'STD00788', 'STD00790', 'STD00793', 'STD00798'] - -CHEM106 -['STD00601', 'STD00602', 'STD00603', 'STD00604', 'STD00608', 'STD00609', 'STD00610', 'STD00611', 'STD00612', 'STD00614', 'STD00616', 'STD00617', 'STD00619', 'STD00621', 'STD00624', 'STD00626', 'STD00630', 'STD00634', 'STD00637', 'STD00638', 'STD00640', 'STD00642', 'STD00645', 'STD00647', 'STD00648', 'STD00652', 'STD00654', 'STD00656', 'STD00658', 'STD00659', 'STD00666', 'STD00670', 'STD00673', 'STD00674', 'STD00675', 'STD00678', 'STD00679', 'STD00684', 'STD00685', 'STD00690', 'STD00693', 'STD00696', 'STD00701', 'STD00703', 'STD00707', 'STD00708', 'STD00709', 'STD00718', 'STD00721', 'STD00723', 'STD00724', 'STD00727', 'STD00729', 'STD00730', 'STD00732', 'STD00739', 'STD00741', 'STD00745', 'STD00746', 'STD00751', 'STD00756', 'STD00759', 'STD00760', 'STD00761', 'STD00762', 'STD00763', 'STD00768', 'STD00769', 'STD00770', 'STD00775', 'STD00779', 'STD00780', 'STD00783', 'STD00785', 'STD00786', 'STD00790', 'STD00793', 'STD00797'] - -CHEM107 -['STD00601', 'STD00602', 'STD00603', 'STD00605', 'STD00608', 'STD00609', 'STD00610', 'STD00612', 'STD00616', 'STD00623', 'STD00624', 'STD00627', 'STD00628', 'STD00637', 'STD00639', 'STD00646', 'STD00649', 'STD00650', 'STD00660', 'STD00663', 'STD00667', 'STD00673', 'STD00674', 'STD00677', 'STD00678', 'STD00679', 'STD00681', 'STD00685', 'STD00686', 'STD00688', 'STD00689', 'STD00692', 'STD00693', 'STD00699', 'STD00701', 'STD00704', 'STD00706', 'STD00707', 'STD00708', 'STD00712', 'STD00713', 'STD00715', 'STD00716', 'STD00717', 'STD00719', 'STD00720', 'STD00722', 'STD00731', 'STD00734', 'STD00735', 'STD00736', 'STD00738', 'STD00745', 'STD00746', 'STD00747', 'STD00748', 'STD00751', 'STD00752', 'STD00756', 'STD00759', 'STD00760', 'STD00765', 'STD00768', 'STD00769', 'STD00772', 'STD00773', 'STD00776', 'STD00777', 'STD00778', 'STD00779', 'STD00780', 'STD00781', 'STD00782', 'STD00795', 'STD00798', 'STD00799'] - -CHEM108 -['STD00601', 'STD00608', 'STD00619', 'STD00622', 'STD00624', 'STD00625', 'STD00627', 'STD00632', 'STD00635', 'STD00636', 'STD00638', 'STD00641', 'STD00643', 'STD00645', 'STD00651', 'STD00654', 'STD00656', 'STD00661', 'STD00662', 'STD00664', 'STD00666', 'STD00667', 'STD00668', 'STD00670', 'STD00671', 'STD00672', 'STD00676', 'STD00679', 'STD00688', 'STD00689', 'STD00690', 'STD00691', 'STD00692', 'STD00698', 'STD00700', 'STD00703', 'STD00704', 'STD00705', 'STD00708', 'STD00711', 'STD00712', 'STD00721', 'STD00726', 'STD00731', 'STD00736', 'STD00738', 'STD00743', 'STD00744', 'STD00747', 'STD00750', 'STD00753', 'STD00755', 'STD00756', 'STD00757', 'STD00758', 'STD00762', 'STD00764', 'STD00770', 'STD00778', 'STD00781', 'STD00787', 'STD00788', 'STD00792', 'STD00794', 'STD00796'] - -CHEM109 -['STD00601', 'STD00602', 'STD00604', 'STD00605', 'STD00608', 'STD00614', 'STD00618', 'STD00623', 'STD00631', 'STD00633', 'STD00637', 'STD00639', 'STD00642', 'STD00644', 'STD00647', 'STD00648', 'STD00649', 'STD00650', 'STD00653', 'STD00661', 'STD00662', 'STD00665', 'STD00666', 'STD00667', 'STD00671', 'STD00676', 'STD00680', 'STD00682', 'STD00684', 'STD00686', 'STD00687', 'STD00690', 'STD00692', 'STD00694', 'STD00695', 'STD00696', 'STD00698', 'STD00702', 'STD00705', 'STD00706', 'STD00707', 'STD00709', 'STD00710', 'STD00713', 'STD00714', 'STD00715', 'STD00716', 'STD00717', 'STD00718', 'STD00727', 'STD00730', 'STD00731', 'STD00732', 'STD00733', 'STD00736', 'STD00737', 'STD00739', 'STD00741', 'STD00742', 'STD00743', 'STD00745', 'STD00749', 'STD00750', 'STD00752', 'STD00758', 'STD00760', 'STD00764', 'STD00765', 'STD00769', 'STD00771', 'STD00775', 'STD00777', 'STD00786', 'STD00787', 'STD00789', 'STD00796', 'STD00798', 'STD00799', 'STD00800'] - -CHEM110 -['STD00604', 'STD00606', 'STD00610', 'STD00611', 'STD00615', 'STD00618', 'STD00619', 'STD00620', 'STD00622', 'STD00627', 'STD00628', 'STD00636', 'STD00638', 'STD00639', 'STD00642', 'STD00645', 'STD00646', 'STD00647', 'STD00650', 'STD00651', 'STD00655', 'STD00657', 'STD00658', 'STD00660', 'STD00663', 'STD00664', 'STD00665', 'STD00667', 'STD00668', 'STD00672', 'STD00675', 'STD00676', 'STD00677', 'STD00681', 'STD00683', 'STD00685', 'STD00686', 'STD00687', 'STD00695', 'STD00696', 'STD00699', 'STD00701', 'STD00702', 'STD00708', 'STD00711', 'STD00720', 'STD00724', 'STD00725', 'STD00726', 'STD00728', 'STD00730', 'STD00733', 'STD00734', 'STD00735', 'STD00737', 'STD00740', 'STD00742', 'STD00746', 'STD00747', 'STD00748', 'STD00749', 'STD00750', 'STD00753', 'STD00755', 'STD00758', 'STD00766', 'STD00773', 'STD00774', 'STD00776', 'STD00779', 'STD00781', 'STD00782', 'STD00786', 'STD00787', 'STD00788', 'STD00789', 'STD00790', 'STD00793', 'STD00794', 'STD00800'] - -BIO101 -['STD00801', 'STD00804', 'STD00805', 'STD00806', 'STD00807', 'STD00809', 'STD00810', 'STD00811', 'STD00812', 'STD00816', 'STD00817', 'STD00822', 'STD00825', 'STD00835', 'STD00837', 'STD00839', 'STD00841', 'STD00842', 'STD00848', 'STD00849', 'STD00850', 'STD00851', 'STD00856', 'STD00857', 'STD00858', 'STD00859', 'STD00860', 'STD00862', 'STD00863', 'STD00867', 'STD00869', 'STD00871', 'STD00872', 'STD00873', 'STD00874', 'STD00877', 'STD00880', 'STD00885', 'STD00887', 'STD00888', 'STD00889', 'STD00890', 'STD00891', 'STD00896', 'STD00897', 'STD00903', 'STD00904', 'STD00906', 'STD00907', 'STD00908', 'STD00909', 'STD00911', 'STD00912', 'STD00913', 'STD00914', 'STD00918', 'STD00920', 'STD00921', 'STD00922', 'STD00923', 'STD00924', 'STD00925', 'STD00926', 'STD00931', 'STD00932', 'STD00934', 'STD00936', 'STD00938', 'STD00941', 'STD00942', 'STD00944', 'STD00946', 'STD00948', 'STD00949', 'STD00951', 'STD00954', 'STD00959', 'STD00960', 'STD00962', 'STD00963', 'STD00964', 'STD00969', 'STD00975', 'STD00976', 'STD00978', 'STD00979', 'STD00982', 'STD00983', 'STD00985', 'STD00990', 'STD00991', 'STD00993', 'STD00996', 'STD00998', 'STD01000'] - -BIO102 -['STD00802', 'STD00807', 'STD00809', 'STD00811', 'STD00812', 'STD00818', 'STD00819', 'STD00822', 'STD00830', 'STD00832', 'STD00833', 'STD00835', 'STD00839', 'STD00840', 'STD00841', 'STD00842', 'STD00845', 'STD00848', 'STD00849', 'STD00858', 'STD00865', 'STD00866', 'STD00867', 'STD00868', 'STD00869', 'STD00870', 'STD00871', 'STD00872', 'STD00873', 'STD00878', 'STD00880', 'STD00882', 'STD00883', 'STD00884', 'STD00886', 'STD00887', 'STD00889', 'STD00890', 'STD00891', 'STD00892', 'STD00896', 'STD00898', 'STD00899', 'STD00900', 'STD00903', 'STD00907', 'STD00908', 'STD00914', 'STD00921', 'STD00923', 'STD00934', 'STD00935', 'STD00936', 'STD00938', 'STD00939', 'STD00940', 'STD00941', 'STD00944', 'STD00945', 'STD00948', 'STD00949', 'STD00951', 'STD00954', 'STD00957', 'STD00959', 'STD00960', 'STD00961', 'STD00963', 'STD00965', 'STD00967', 'STD00970', 'STD00971', 'STD00974', 'STD00975', 'STD00976', 'STD00977', 'STD00982', 'STD00987', 'STD00990', 'STD00992', 'STD00993', 'STD00994', 'STD00997', 'STD00998', 'STD00999'] - -BIO103 -['STD00801', 'STD00802', 'STD00803', 'STD00807', 'STD00808', 'STD00810', 'STD00812', 'STD00814', 'STD00818', 'STD00819', 'STD00820', 'STD00821', 'STD00827', 'STD00828', 'STD00830', 'STD00832', 'STD00836', 'STD00838', 'STD00840', 'STD00841', 'STD00842', 'STD00843', 'STD00844', 'STD00845', 'STD00846', 'STD00848', 'STD00850', 'STD00852', 'STD00854', 'STD00855', 'STD00862', 'STD00864', 'STD00865', 'STD00866', 'STD00868', 'STD00870', 'STD00873', 'STD00876', 'STD00878', 'STD00881', 'STD00883', 'STD00884', 'STD00886', 'STD00890', 'STD00892', 'STD00893', 'STD00897', 'STD00898', 'STD00900', 'STD00905', 'STD00908', 'STD00912', 'STD00913', 'STD00915', 'STD00916', 'STD00917', 'STD00923', 'STD00927', 'STD00932', 'STD00933', 'STD00934', 'STD00935', 'STD00938', 'STD00942', 'STD00945', 'STD00946', 'STD00953', 'STD00955', 'STD00956', 'STD00957', 'STD00958', 'STD00959', 'STD00960', 'STD00961', 'STD00963', 'STD00964', 'STD00968', 'STD00970', 'STD00971', 'STD00973', 'STD00977', 'STD00980', 'STD00982', 'STD00984', 'STD00985', 'STD00986', 'STD00995', 'STD00996', 'STD00997'] - -BIO104 -['STD00801', 'STD00803', 'STD00804', 'STD00805', 'STD00806', 'STD00808', 'STD00809', 'STD00810', 'STD00815', 'STD00817', 'STD00820', 'STD00821', 'STD00824', 'STD00825', 'STD00826', 'STD00828', 'STD00831', 'STD00837', 'STD00841', 'STD00844', 'STD00846', 'STD00847', 'STD00853', 'STD00854', 'STD00855', 'STD00856', 'STD00858', 'STD00860', 'STD00861', 'STD00862', 'STD00863', 'STD00866', 'STD00870', 'STD00872', 'STD00876', 'STD00879', 'STD00881', 'STD00883', 'STD00886', 'STD00888', 'STD00890', 'STD00891', 'STD00893', 'STD00894', 'STD00895', 'STD00899', 'STD00901', 'STD00902', 'STD00905', 'STD00913', 'STD00915', 'STD00917', 'STD00918', 'STD00919', 'STD00920', 'STD00922', 'STD00925', 'STD00926', 'STD00927', 'STD00928', 'STD00929', 'STD00930', 'STD00932', 'STD00933', 'STD00935', 'STD00938', 'STD00939', 'STD00941', 'STD00950', 'STD00951', 'STD00952', 'STD00953', 'STD00955', 'STD00956', 'STD00958', 'STD00959', 'STD00962', 'STD00965', 'STD00968', 'STD00969', 'STD00970', 'STD00971', 'STD00973', 'STD00974', 'STD00978', 'STD00979', 'STD00980', 'STD00981', 'STD00982', 'STD00984', 'STD00986', 'STD00988', 'STD00991', 'STD00995', 'STD00996', 'STD00997'] - -BIO105 -['STD00803', 'STD00814', 'STD00815', 'STD00820', 'STD00821', 'STD00824', 'STD00832', 'STD00833', 'STD00835', 'STD00836', 'STD00837', 'STD00844', 'STD00847', 'STD00850', 'STD00853', 'STD00857', 'STD00862', 'STD00863', 'STD00865', 'STD00875', 'STD00878', 'STD00882', 'STD00885', 'STD00888', 'STD00892', 'STD00893', 'STD00895', 'STD00896', 'STD00898', 'STD00900', 'STD00901', 'STD00902', 'STD00904', 'STD00911', 'STD00919', 'STD00921', 'STD00922', 'STD00924', 'STD00925', 'STD00927', 'STD00928', 'STD00929', 'STD00930', 'STD00931', 'STD00932', 'STD00933', 'STD00937', 'STD00940', 'STD00946', 'STD00947', 'STD00950', 'STD00954', 'STD00956', 'STD00957', 'STD00964', 'STD00975', 'STD00981', 'STD00986', 'STD00987', 'STD00988', 'STD00991', 'STD00992'] - -BIO106 -['STD00813', 'STD00815', 'STD00816', 'STD00817', 'STD00819', 'STD00824', 'STD00826', 'STD00827', 'STD00829', 'STD00830', 'STD00831', 'STD00836', 'STD00838', 'STD00840', 'STD00842', 'STD00851', 'STD00852', 'STD00856', 'STD00858', 'STD00863', 'STD00867', 'STD00869', 'STD00873', 'STD00874', 'STD00875', 'STD00880', 'STD00881', 'STD00882', 'STD00884', 'STD00888', 'STD00889', 'STD00891', 'STD00894', 'STD00895', 'STD00900', 'STD00901', 'STD00903', 'STD00906', 'STD00910', 'STD00911', 'STD00912', 'STD00913', 'STD00914', 'STD00917', 'STD00920', 'STD00922', 'STD00924', 'STD00926', 'STD00927', 'STD00928', 'STD00930', 'STD00936', 'STD00937', 'STD00940', 'STD00943', 'STD00945', 'STD00946', 'STD00947', 'STD00948', 'STD00949', 'STD00952', 'STD00953', 'STD00955', 'STD00957', 'STD00958', 'STD00962', 'STD00965', 'STD00966', 'STD00968', 'STD00970', 'STD00972', 'STD00973', 'STD00977', 'STD00979', 'STD00981', 'STD00985', 'STD00987', 'STD00988', 'STD00989', 'STD00990', 'STD00992', 'STD00993', 'STD00994', 'STD00995', 'STD00998', 'STD00999'] - -BIO107 -['STD00802', 'STD00805', 'STD00806', 'STD00808', 'STD00811', 'STD00813', 'STD00814', 'STD00815', 'STD00816', 'STD00818', 'STD00821', 'STD00823', 'STD00825', 'STD00827', 'STD00829', 'STD00831', 'STD00833', 'STD00834', 'STD00837', 'STD00838', 'STD00845', 'STD00846', 'STD00850', 'STD00853', 'STD00854', 'STD00855', 'STD00859', 'STD00860', 'STD00861', 'STD00871', 'STD00874', 'STD00876', 'STD00877', 'STD00878', 'STD00879', 'STD00880', 'STD00882', 'STD00884', 'STD00885', 'STD00895', 'STD00897', 'STD00902', 'STD00904', 'STD00906', 'STD00909', 'STD00910', 'STD00911', 'STD00914', 'STD00915', 'STD00916', 'STD00919', 'STD00923', 'STD00929', 'STD00931', 'STD00933', 'STD00943', 'STD00947', 'STD00948', 'STD00949', 'STD00966', 'STD00967', 'STD00969', 'STD00975', 'STD00978', 'STD00985', 'STD00988', 'STD00989', 'STD00999', 'STD01000'] - -BIO108 -['STD00804', 'STD00805', 'STD00808', 'STD00811', 'STD00813', 'STD00814', 'STD00816', 'STD00822', 'STD00823', 'STD00826', 'STD00828', 'STD00829', 'STD00834', 'STD00835', 'STD00839', 'STD00843', 'STD00845', 'STD00846', 'STD00851', 'STD00852', 'STD00854', 'STD00855', 'STD00861', 'STD00864', 'STD00865', 'STD00866', 'STD00867', 'STD00868', 'STD00871', 'STD00874', 'STD00879', 'STD00881', 'STD00883', 'STD00885', 'STD00886', 'STD00887', 'STD00889', 'STD00898', 'STD00899', 'STD00901', 'STD00905', 'STD00907', 'STD00918', 'STD00919', 'STD00921', 'STD00924', 'STD00925', 'STD00926', 'STD00928', 'STD00929', 'STD00934', 'STD00935', 'STD00939', 'STD00940', 'STD00942', 'STD00943', 'STD00945', 'STD00950', 'STD00952', 'STD00953', 'STD00955', 'STD00958', 'STD00960', 'STD00966', 'STD00967', 'STD00969', 'STD00972', 'STD00974', 'STD00976', 'STD00977', 'STD00978', 'STD00980', 'STD00983', 'STD00984', 'STD00986', 'STD00987', 'STD00989', 'STD00990', 'STD00991', 'STD00993', 'STD00994', 'STD00998', 'STD00999', 'STD01000'] - -BIO109 -['STD00803', 'STD00807', 'STD00812', 'STD00818', 'STD00819', 'STD00820', 'STD00822', 'STD00823', 'STD00825', 'STD00827', 'STD00828', 'STD00831', 'STD00834', 'STD00836', 'STD00839', 'STD00843', 'STD00847', 'STD00849', 'STD00852', 'STD00857', 'STD00859', 'STD00864', 'STD00869', 'STD00875', 'STD00877', 'STD00894', 'STD00899', 'STD00903', 'STD00904', 'STD00906', 'STD00907', 'STD00909', 'STD00910', 'STD00912', 'STD00916', 'STD00918', 'STD00920', 'STD00931', 'STD00937', 'STD00939', 'STD00941', 'STD00942', 'STD00944', 'STD00950', 'STD00952', 'STD00954', 'STD00961', 'STD00963', 'STD00968', 'STD00972', 'STD00973', 'STD00974', 'STD00979', 'STD00981', 'STD00983', 'STD00989', 'STD00992', 'STD00995'] - -BIO110 -['STD00801', 'STD00802', 'STD00804', 'STD00806', 'STD00809', 'STD00810', 'STD00813', 'STD00817', 'STD00823', 'STD00824', 'STD00826', 'STD00829', 'STD00830', 'STD00832', 'STD00833', 'STD00834', 'STD00838', 'STD00840', 'STD00843', 'STD00844', 'STD00847', 'STD00848', 'STD00849', 'STD00851', 'STD00853', 'STD00856', 'STD00857', 'STD00859', 'STD00860', 'STD00861', 'STD00864', 'STD00868', 'STD00870', 'STD00872', 'STD00875', 'STD00876', 'STD00877', 'STD00879', 'STD00887', 'STD00892', 'STD00893', 'STD00894', 'STD00896', 'STD00897', 'STD00902', 'STD00905', 'STD00908', 'STD00909', 'STD00910', 'STD00915', 'STD00916', 'STD00917', 'STD00930', 'STD00936', 'STD00937', 'STD00943', 'STD00944', 'STD00947', 'STD00951', 'STD00956', 'STD00961', 'STD00962', 'STD00964', 'STD00965', 'STD00966', 'STD00967', 'STD00971', 'STD00972', 'STD00976', 'STD00980', 'STD00983', 'STD00984', 'STD00994', 'STD00996', 'STD00997', 'STD01000'] - diff --git a/sample_data/duzeltattendance.csv b/sample_data/duzeltattendance.csv new file mode 100644 index 0000000..4d6f9b4 --- /dev/null +++ b/sample_data/duzeltattendance.csv @@ -0,0 +1,149 @@ +CS101 +['Std_ID_170', 'Std_ID_003', 'Std_ID_007', 'Std_ID_008', 'Std_ID_013', 'Std_ID_014', 'Std_ID_015', 'Std_ID_016', 'Std_ID_018', 'Std_ID_024', 'Std_ID_025', 'Std_ID_026', 'Std_ID_028', 'Std_ID_029', 'Std_ID_031', 'Std_ID_032', 'Std_ID_037', 'Std_ID_039', 'Std_ID_041', 'Std_ID_042', 'Std_ID_043', 'Std_ID_046', 'Std_ID_047', 'Std_ID_049', 'Std_ID_050', 'Std_ID_051', 'Std_ID_052', 'Std_ID_054', 'Std_ID_055', 'Std_ID_060', 'Std_ID_063', 'Std_ID_065', 'Std_ID_070', 'Std_ID_073', 'Std_ID_074', 'Std_ID_075', 'Std_ID_076', 'Std_ID_077', 'Std_ID_078', 'Std_ID_081', 'Std_ID_083', 'Std_ID_084', 'Std_ID_086', 'Std_ID_087', 'Std_ID_088', 'Std_ID_089', 'Std_ID_090', 'Std_ID_091', 'Std_ID_092', 'Std_ID_093', 'Std_ID_094', 'Std_ID_098', 'Std_ID_102', 'Std_ID_103', 'Std_ID_104', 'Std_ID_105', 'Std_ID_106', 'Std_ID_107', 'Std_ID_108', 'Std_ID_109', 'Std_ID_111', 'Std_ID_113', 'Std_ID_115', 'Std_ID_116', 'Std_ID_119', 'Std_ID_122', 'Std_ID_124', 'Std_ID_125', 'Std_ID_128', 'Std_ID_129', 'Std_ID_130', 'Std_ID_132', 'Std_ID_133', 'Std_ID_136', 'Std_ID_138', 'Std_ID_143', 'Std_ID_149', 'Std_ID_150', 'Std_ID_153', 'Std_ID_155', 'Std_ID_156', 'Std_ID_159', 'Std_ID_160', 'Std_ID_161', 'Std_ID_163', 'Std_ID_165', 'Std_ID_166', 'Std_ID_169', 'Std_ID_171', 'Std_ID_173', 'Std_ID_176', 'Std_ID_178', 'Std_ID_179', 'Std_ID_180', 'Std_ID_182', 'Std_ID_183', 'Std_ID_184', 'Std_ID_188', 'Std_ID_189', 'Std_ID_192', 'Std_ID_196', 'Std_ID_197'] + +CS102 +['Std_ID_001', 'Std_ID_003', 'Std_ID_005', 'Std_ID_007', 'Std_ID_010', 'Std_ID_011', 'Std_ID_012', 'Std_ID_016', 'Std_ID_019', 'Std_ID_021', 'Std_ID_024', 'Std_ID_025', 'Std_ID_026', 'Std_ID_029', 'Std_ID_032', 'Std_ID_033', 'Std_ID_036', 'Std_ID_037', 'Std_ID_039', 'Std_ID_040', 'Std_ID_041', 'Std_ID_046', 'Std_ID_047', 'Std_ID_051', 'Std_ID_054', 'Std_ID_055', 'Std_ID_058', 'Std_ID_059', 'Std_ID_060', 'Std_ID_065', 'Std_ID_066', 'Std_ID_067', 'Std_ID_072', 'Std_ID_075', 'Std_ID_076', 'Std_ID_077', 'Std_ID_078', 'Std_ID_080', 'Std_ID_081', 'Std_ID_082', 'Std_ID_085', 'Std_ID_086', 'Std_ID_088', 'Std_ID_092', 'Std_ID_094', 'Std_ID_095', 'Std_ID_098', 'Std_ID_099', 'Std_ID_100', 'Std_ID_101', 'Std_ID_112', 'Std_ID_115', 'Std_ID_120', 'Std_ID_121', 'Std_ID_123', 'Std_ID_124', 'Std_ID_127', 'Std_ID_128', 'Std_ID_132', 'Std_ID_133', 'Std_ID_135', 'Std_ID_136', 'Std_ID_138', 'Std_ID_140', 'Std_ID_142', 'Std_ID_145', 'Std_ID_148', 'Std_ID_150', 'Std_ID_152', 'Std_ID_153', 'Std_ID_155', 'Std_ID_156', 'Std_ID_157', 'Std_ID_160', 'Std_ID_161', 'Std_ID_164', 'Std_ID_165', 'Std_ID_168', 'Std_ID_169', 'Std_ID_175', 'Std_ID_176', 'Std_ID_182', 'Std_ID_184', 'Std_ID_188', 'Std_ID_189', 'Std_ID_190', 'Std_ID_191', 'Std_ID_192', 'Std_ID_197', 'Std_ID_200'] + +CS103 +['Std_ID_006', 'Std_ID_009', 'Std_ID_011', 'Std_ID_014', 'Std_ID_015', 'Std_ID_017', 'Std_ID_019', 'Std_ID_020', 'Std_ID_021', 'Std_ID_022', 'Std_ID_026', 'Std_ID_027', 'Std_ID_029', 'Std_ID_030', 'Std_ID_031', 'Std_ID_036', 'Std_ID_038', 'Std_ID_039', 'Std_ID_040', 'Std_ID_041', 'Std_ID_045', 'Std_ID_046', 'Std_ID_048', 'Std_ID_050', 'Std_ID_056', 'Std_ID_060', 'Std_ID_061', 'Std_ID_062', 'Std_ID_066', 'Std_ID_067', 'Std_ID_071', 'Std_ID_077', 'Std_ID_079', 'Std_ID_080', 'Std_ID_082', 'Std_ID_085', 'Std_ID_087', 'Std_ID_090', 'Std_ID_096', 'Std_ID_102', 'Std_ID_103', 'Std_ID_104', 'Std_ID_107', 'Std_ID_110', 'Std_ID_112', 'Std_ID_114', 'Std_ID_117', 'Std_ID_118', 'Std_ID_120', 'Std_ID_121', 'Std_ID_123', 'Std_ID_128', 'Std_ID_131', 'Std_ID_134', 'Std_ID_135', 'Std_ID_138', 'Std_ID_140', 'Std_ID_144', 'Std_ID_145', 'Std_ID_146', 'Std_ID_147', 'Std_ID_148', 'Std_ID_149', 'Std_ID_151', 'Std_ID_153', 'Std_ID_154', 'Std_ID_157', 'Std_ID_165', 'Std_ID_166', 'Std_ID_169', 'Std_ID_174', 'Std_ID_175', 'Std_ID_177', 'Std_ID_179', 'Std_ID_190', 'Std_ID_193', 'Std_ID_194', 'Std_ID_198', 'Std_ID_200'] + +CS104 +['Std_ID_001', 'Std_ID_004', 'Std_ID_005', 'Std_ID_009', 'Std_ID_020', 'Std_ID_022', 'Std_ID_023', 'Std_ID_024', 'Std_ID_025', 'Std_ID_027', 'Std_ID_028', 'Std_ID_030', 'Std_ID_034', 'Std_ID_035', 'Std_ID_036', 'Std_ID_048', 'Std_ID_052', 'Std_ID_053', 'Std_ID_056', 'Std_ID_058', 'Std_ID_060', 'Std_ID_061', 'Std_ID_062', 'Std_ID_064', 'Std_ID_065', 'Std_ID_066', 'Std_ID_069', 'Std_ID_071', 'Std_ID_073', 'Std_ID_074', 'Std_ID_076', 'Std_ID_079', 'Std_ID_082', 'Std_ID_083', 'Std_ID_084', 'Std_ID_086', 'Std_ID_088', 'Std_ID_089', 'Std_ID_090', 'Std_ID_093', 'Std_ID_095', 'Std_ID_097', 'Std_ID_098', 'Std_ID_101', 'Std_ID_106', 'Std_ID_108', 'Std_ID_109', 'Std_ID_110', 'Std_ID_111', 'Std_ID_114', 'Std_ID_117', 'Std_ID_118', 'Std_ID_119', 'Std_ID_123', 'Std_ID_127', 'Std_ID_132', 'Std_ID_134', 'Std_ID_135', 'Std_ID_136', 'Std_ID_139', 'Std_ID_140', 'Std_ID_142', 'Std_ID_143', 'Std_ID_144', 'Std_ID_145', 'Std_ID_146', 'Std_ID_150', 'Std_ID_151', 'Std_ID_152', 'Std_ID_156', 'Std_ID_157', 'Std_ID_162', 'Std_ID_163', 'Std_ID_164', 'Std_ID_166', 'Std_ID_168', 'Std_ID_175', 'Std_ID_177', 'Std_ID_178', 'Std_ID_180', 'Std_ID_181', 'Std_ID_185', 'Std_ID_186', 'Std_ID_190', 'Std_ID_193', 'Std_ID_196', 'Std_ID_198', 'Std_ID_199', 'Std_ID_200'] + +CS105 +['Std_ID_002', 'Std_ID_010', 'Std_ID_011', 'Std_ID_012', 'Std_ID_014', 'Std_ID_019', 'Std_ID_022', 'Std_ID_030', 'Std_ID_031', 'Std_ID_035', 'Std_ID_038', 'Std_ID_039', 'Std_ID_042', 'Std_ID_043', 'Std_ID_044', 'Std_ID_045', 'Std_ID_049', 'Std_ID_053', 'Std_ID_055', 'Std_ID_057', 'Std_ID_059', 'Std_ID_061', 'Std_ID_064', 'Std_ID_068', 'Std_ID_070', 'Std_ID_083', 'Std_ID_093', 'Std_ID_096', 'Std_ID_104', 'Std_ID_107', 'Std_ID_122', 'Std_ID_129', 'Std_ID_131', 'Std_ID_133', 'Std_ID_137', 'Std_ID_139', 'Std_ID_146', 'Std_ID_154', 'Std_ID_156', 'Std_ID_158', 'Std_ID_167', 'Std_ID_170', 'Std_ID_171', 'Std_ID_172', 'Std_ID_173', 'Std_ID_174', 'Std_ID_175', 'Std_ID_178', 'Std_ID_180', 'Std_ID_181', 'Std_ID_183', 'Std_ID_184', 'Std_ID_185', 'Std_ID_187', 'Std_ID_191', 'Std_ID_193', 'Std_ID_194', 'Std_ID_197', 'Std_ID_198', 'Std_ID_199'] + +CS106 +['Std_ID_005', 'Std_ID_008', 'Std_ID_009', 'Std_ID_010', 'Std_ID_015', 'Std_ID_018', 'Std_ID_019', 'Std_ID_020', 'Std_ID_025', 'Std_ID_026', 'Std_ID_032', 'Std_ID_033', 'Std_ID_034', 'Std_ID_037', 'Std_ID_042', 'Std_ID_043', 'Std_ID_044', 'Std_ID_047', 'Std_ID_048', 'Std_ID_052', 'Std_ID_053', 'Std_ID_056', 'Std_ID_057', 'Std_ID_058', 'Std_ID_059', 'Std_ID_062', 'Std_ID_064', 'Std_ID_067', 'Std_ID_069', 'Std_ID_070', 'Std_ID_075', 'Std_ID_076', 'Std_ID_079', 'Std_ID_080', 'Std_ID_081', 'Std_ID_083', 'Std_ID_085', 'Std_ID_089', 'Std_ID_092', 'Std_ID_093', 'Std_ID_095', 'Std_ID_096', 'Std_ID_097', 'Std_ID_102', 'Std_ID_103', 'Std_ID_106', 'Std_ID_107', 'Std_ID_108', 'Std_ID_110', 'Std_ID_116', 'Std_ID_118', 'Std_ID_121', 'Std_ID_123', 'Std_ID_124', 'Std_ID_129', 'Std_ID_130', 'Std_ID_131', 'Std_ID_133', 'Std_ID_134', 'Std_ID_137', 'Std_ID_139', 'Std_ID_141', 'Std_ID_142', 'Std_ID_143', 'Std_ID_145', 'Std_ID_147', 'Std_ID_148', 'Std_ID_149', 'Std_ID_150', 'Std_ID_151', 'Std_ID_152', 'Std_ID_154', 'Std_ID_157', 'Std_ID_158', 'Std_ID_159', 'Std_ID_160', 'Std_ID_161', 'Std_ID_162', 'Std_ID_163', 'Std_ID_164', 'Std_ID_167', 'Std_ID_168', 'Std_ID_170', 'Std_ID_172', 'Std_ID_182', 'Std_ID_185', 'Std_ID_186', 'Std_ID_187', 'Std_ID_193', 'Std_ID_194', 'Std_ID_195', 'Std_ID_196'] + +CS107 +['Std_ID_001', 'Std_ID_002', 'Std_ID_003', 'Std_ID_004', 'Std_ID_006', 'Std_ID_007', 'Std_ID_013', 'Std_ID_016', 'Std_ID_017', 'Std_ID_018', 'Std_ID_024', 'Std_ID_028', 'Std_ID_030', 'Std_ID_034', 'Std_ID_035', 'Std_ID_036', 'Std_ID_038', 'Std_ID_044', 'Std_ID_045', 'Std_ID_049', 'Std_ID_051', 'Std_ID_054', 'Std_ID_056', 'Std_ID_058', 'Std_ID_066', 'Std_ID_068', 'Std_ID_071', 'Std_ID_072', 'Std_ID_074', 'Std_ID_078', 'Std_ID_084', 'Std_ID_085', 'Std_ID_087', 'Std_ID_091', 'Std_ID_092', 'Std_ID_094', 'Std_ID_095', 'Std_ID_101', 'Std_ID_105', 'Std_ID_106', 'Std_ID_110', 'Std_ID_111', 'Std_ID_112', 'Std_ID_113', 'Std_ID_116', 'Std_ID_120', 'Std_ID_122', 'Std_ID_126', 'Std_ID_128', 'Std_ID_136', 'Std_ID_137', 'Std_ID_141', 'Std_ID_144', 'Std_ID_146', 'Std_ID_151', 'Std_ID_155', 'Std_ID_158', 'Std_ID_160', 'Std_ID_161', 'Std_ID_163', 'Std_ID_164', 'Std_ID_169', 'Std_ID_170', 'Std_ID_173', 'Std_ID_176', 'Std_ID_179', 'Std_ID_181', 'Std_ID_184', 'Std_ID_194', 'Std_ID_195', 'Std_ID_199', 'Std_ID_200'] + +CS108 +['Std_ID_001', 'Std_ID_003', 'Std_ID_004', 'Std_ID_006', 'Std_ID_008', 'Std_ID_011', 'Std_ID_012', 'Std_ID_013', 'Std_ID_018', 'Std_ID_022', 'Std_ID_023', 'Std_ID_033', 'Std_ID_034', 'Std_ID_035', 'Std_ID_040', 'Std_ID_041', 'Std_ID_043', 'Std_ID_044', 'Std_ID_045', 'Std_ID_050', 'Std_ID_052', 'Std_ID_054', 'Std_ID_057', 'Std_ID_063', 'Std_ID_065', 'Std_ID_067', 'Std_ID_069', 'Std_ID_070', 'Std_ID_073', 'Std_ID_075', 'Std_ID_078', 'Std_ID_079', 'Std_ID_080', 'Std_ID_088', 'Std_ID_091', 'Std_ID_094', 'Std_ID_096', 'Std_ID_097', 'Std_ID_098', 'Std_ID_099', 'Std_ID_100', 'Std_ID_103', 'Std_ID_109', 'Std_ID_111', 'Std_ID_112', 'Std_ID_113', 'Std_ID_116', 'Std_ID_117', 'Std_ID_119', 'Std_ID_120', 'Std_ID_121', 'Std_ID_125', 'Std_ID_126', 'Std_ID_127', 'Std_ID_130', 'Std_ID_135', 'Std_ID_139', 'Std_ID_141', 'Std_ID_144', 'Std_ID_147', 'Std_ID_149', 'Std_ID_153', 'Std_ID_159', 'Std_ID_162', 'Std_ID_168', 'Std_ID_170', 'Std_ID_171', 'Std_ID_174', 'Std_ID_177', 'Std_ID_178', 'Std_ID_180', 'Std_ID_183', 'Std_ID_186', 'Std_ID_187', 'Std_ID_189', 'Std_ID_191', 'Std_ID_192', 'Std_ID_195', 'Std_ID_197'] + +CS109 +['Std_ID_004', 'Std_ID_006', 'Std_ID_008', 'Std_ID_012', 'Std_ID_016', 'Std_ID_017', 'Std_ID_021', 'Std_ID_023', 'Std_ID_027', 'Std_ID_029', 'Std_ID_031', 'Std_ID_032', 'Std_ID_037', 'Std_ID_038', 'Std_ID_040', 'Std_ID_042', 'Std_ID_047', 'Std_ID_059', 'Std_ID_062', 'Std_ID_063', 'Std_ID_068', 'Std_ID_072', 'Std_ID_073', 'Std_ID_074', 'Std_ID_081', 'Std_ID_082', 'Std_ID_086', 'Std_ID_087', 'Std_ID_091', 'Std_ID_097', 'Std_ID_099', 'Std_ID_100', 'Std_ID_101', 'Std_ID_104', 'Std_ID_105', 'Std_ID_109', 'Std_ID_114', 'Std_ID_115', 'Std_ID_119', 'Std_ID_122', 'Std_ID_124', 'Std_ID_125', 'Std_ID_126', 'Std_ID_129', 'Std_ID_130', 'Std_ID_131', 'Std_ID_134', 'Std_ID_137', 'Std_ID_141', 'Std_ID_142', 'Std_ID_147', 'Std_ID_148', 'Std_ID_152', 'Std_ID_159', 'Std_ID_162', 'Std_ID_166', 'Std_ID_167', 'Std_ID_172', 'Std_ID_177', 'Std_ID_179', 'Std_ID_182', 'Std_ID_183', 'Std_ID_185', 'Std_ID_186', 'Std_ID_188', 'Std_ID_199'] + +CS110 +['Std_ID_002', 'Std_ID_005', 'Std_ID_007', 'Std_ID_009', 'Std_ID_010', 'Std_ID_013', 'Std_ID_014', 'Std_ID_015', 'Std_ID_017', 'Std_ID_020', 'Std_ID_021', 'Std_ID_023', 'Std_ID_027', 'Std_ID_028', 'Std_ID_033', 'Std_ID_046', 'Std_ID_048', 'Std_ID_049', 'Std_ID_050', 'Std_ID_051', 'Std_ID_053', 'Std_ID_055', 'Std_ID_057', 'Std_ID_061', 'Std_ID_063', 'Std_ID_064', 'Std_ID_068', 'Std_ID_069', 'Std_ID_071', 'Std_ID_072', 'Std_ID_077', 'Std_ID_084', 'Std_ID_089', 'Std_ID_090', 'Std_ID_099', 'Std_ID_100', 'Std_ID_102', 'Std_ID_105', 'Std_ID_108', 'Std_ID_113', 'Std_ID_114', 'Std_ID_115', 'Std_ID_117', 'Std_ID_118', 'Std_ID_125', 'Std_ID_126', 'Std_ID_127', 'Std_ID_132', 'Std_ID_138', 'Std_ID_140', 'Std_ID_143', 'Std_ID_154', 'Std_ID_155', 'Std_ID_158', 'Std_ID_165', 'Std_ID_167', 'Std_ID_171', 'Std_ID_172', 'Std_ID_173', 'Std_ID_174', 'Std_ID_176', 'Std_ID_181', 'Std_ID_187', 'Std_ID_188', 'Std_ID_189', 'Std_ID_190', 'Std_ID_191', 'Std_ID_192', 'Std_ID_195', 'Std_ID_196', 'Std_ID_198'] + +MATH101 +['Std_ID_201', 'Std_ID_202', 'Std_ID_206', 'Std_ID_207', 'Std_ID_211', 'Std_ID_214', 'Std_ID_215', 'Std_ID_216', 'Std_ID_220', 'Std_ID_221', 'Std_ID_222', 'Std_ID_224', 'Std_ID_225', 'Std_ID_226', 'Std_ID_227', 'Std_ID_228', 'Std_ID_230', 'Std_ID_233', 'Std_ID_235', 'Std_ID_236', 'Std_ID_237', 'Std_ID_240', 'Std_ID_244', 'Std_ID_248', 'Std_ID_249', 'Std_ID_251', 'Std_ID_256', 'Std_ID_258', 'Std_ID_261', 'Std_ID_262', 'Std_ID_264', 'Std_ID_266', 'Std_ID_267', 'Std_ID_271', 'Std_ID_273', 'Std_ID_275', 'Std_ID_281', 'Std_ID_284', 'Std_ID_288', 'Std_ID_289', 'Std_ID_290', 'Std_ID_293', 'Std_ID_294', 'Std_ID_296', 'Std_ID_298', 'Std_ID_302', 'Std_ID_304', 'Std_ID_305', 'Std_ID_306', 'Std_ID_307', 'Std_ID_309', 'Std_ID_311', 'Std_ID_316', 'Std_ID_317', 'Std_ID_318', 'Std_ID_321', 'Std_ID_322', 'Std_ID_323', 'Std_ID_326', 'Std_ID_327', 'Std_ID_328', 'Std_ID_330', 'Std_ID_331', 'Std_ID_332', 'Std_ID_335', 'Std_ID_337', 'Std_ID_338', 'Std_ID_339', 'Std_ID_345', 'Std_ID_346', 'Std_ID_347', 'Std_ID_349', 'Std_ID_350', 'Std_ID_351', 'Std_ID_353', 'Std_ID_355', 'Std_ID_362', 'Std_ID_363', 'Std_ID_364', 'Std_ID_366', 'Std_ID_372', 'Std_ID_374', 'Std_ID_376', 'Std_ID_377', 'Std_ID_378', 'Std_ID_382', 'Std_ID_385', 'Std_ID_387', 'Std_ID_388', 'Std_ID_389', 'Std_ID_391', 'Std_ID_392', 'Std_ID_397', 'Std_ID_398'] + +MATH102 +['Std_ID_201', 'Std_ID_202', 'Std_ID_203', 'Std_ID_204', 'Std_ID_206', 'Std_ID_209', 'Std_ID_210', 'Std_ID_211', 'Std_ID_212', 'Std_ID_214', 'Std_ID_218', 'Std_ID_219', 'Std_ID_220', 'Std_ID_223', 'Std_ID_225', 'Std_ID_228', 'Std_ID_233', 'Std_ID_234', 'Std_ID_235', 'Std_ID_236', 'Std_ID_237', 'Std_ID_238', 'Std_ID_239', 'Std_ID_240', 'Std_ID_243', 'Std_ID_245', 'Std_ID_247', 'Std_ID_248', 'Std_ID_249', 'Std_ID_255', 'Std_ID_256', 'Std_ID_257', 'Std_ID_259', 'Std_ID_261', 'Std_ID_264', 'Std_ID_265', 'Std_ID_267', 'Std_ID_272', 'Std_ID_280', 'Std_ID_281', 'Std_ID_282', 'Std_ID_284', 'Std_ID_285', 'Std_ID_288', 'Std_ID_291', 'Std_ID_292', 'Std_ID_293', 'Std_ID_296', 'Std_ID_301', 'Std_ID_303', 'Std_ID_304', 'Std_ID_305', 'Std_ID_307', 'Std_ID_309', 'Std_ID_311', 'Std_ID_315', 'Std_ID_316', 'Std_ID_318', 'Std_ID_321', 'Std_ID_323', 'Std_ID_325', 'Std_ID_326', 'Std_ID_328', 'Std_ID_329', 'Std_ID_332', 'Std_ID_333', 'Std_ID_335', 'Std_ID_336', 'Std_ID_337', 'Std_ID_338', 'Std_ID_343', 'Std_ID_345', 'Std_ID_346', 'Std_ID_347', 'Std_ID_351', 'Std_ID_354', 'Std_ID_357', 'Std_ID_359', 'Std_ID_360', 'Std_ID_361', 'Std_ID_362', 'Std_ID_365', 'Std_ID_366', 'Std_ID_367', 'Std_ID_369', 'Std_ID_370', 'Std_ID_371', 'Std_ID_373', 'Std_ID_374', 'Std_ID_375', 'Std_ID_376', 'Std_ID_382', 'Std_ID_384', 'Std_ID_385', 'Std_ID_386', 'Std_ID_387', 'Std_ID_389', 'Std_ID_391', 'Std_ID_392', 'Std_ID_395', 'Std_ID_400'] + +MATH103 +['Std_ID_204', 'Std_ID_205', 'Std_ID_209', 'Std_ID_211', 'Std_ID_212', 'Std_ID_216', 'Std_ID_218', 'Std_ID_228', 'Std_ID_230', 'Std_ID_231', 'Std_ID_233', 'Std_ID_234', 'Std_ID_235', 'Std_ID_238', 'Std_ID_242', 'Std_ID_247', 'Std_ID_253', 'Std_ID_255', 'Std_ID_259', 'Std_ID_265', 'Std_ID_267', 'Std_ID_272', 'Std_ID_274', 'Std_ID_279', 'Std_ID_285', 'Std_ID_287', 'Std_ID_288', 'Std_ID_291', 'Std_ID_293', 'Std_ID_295', 'Std_ID_299', 'Std_ID_300', 'Std_ID_303', 'Std_ID_308', 'Std_ID_312', 'Std_ID_321', 'Std_ID_324', 'Std_ID_328', 'Std_ID_330', 'Std_ID_332', 'Std_ID_333', 'Std_ID_334', 'Std_ID_336', 'Std_ID_340', 'Std_ID_341', 'Std_ID_347', 'Std_ID_350', 'Std_ID_352', 'Std_ID_353', 'Std_ID_354', 'Std_ID_356', 'Std_ID_357', 'Std_ID_361', 'Std_ID_363', 'Std_ID_367', 'Std_ID_368', 'Std_ID_370', 'Std_ID_371', 'Std_ID_374', 'Std_ID_379', 'Std_ID_380', 'Std_ID_381', 'Std_ID_383', 'Std_ID_384', 'Std_ID_389', 'Std_ID_395', 'Std_ID_398', 'Std_ID_400'] + +MATH104 +['Std_ID_203', 'Std_ID_205', 'Std_ID_208', 'Std_ID_210', 'Std_ID_212', 'Std_ID_213', 'Std_ID_215', 'Std_ID_219', 'Std_ID_221', 'Std_ID_224', 'Std_ID_226', 'Std_ID_227', 'Std_ID_231', 'Std_ID_232', 'Std_ID_234', 'Std_ID_237', 'Std_ID_242', 'Std_ID_243', 'Std_ID_244', 'Std_ID_245', 'Std_ID_246', 'Std_ID_247', 'Std_ID_249', 'Std_ID_250', 'Std_ID_251', 'Std_ID_255', 'Std_ID_260', 'Std_ID_263', 'Std_ID_264', 'Std_ID_266', 'Std_ID_269', 'Std_ID_270', 'Std_ID_271', 'Std_ID_274', 'Std_ID_276', 'Std_ID_278', 'Std_ID_279', 'Std_ID_280', 'Std_ID_281', 'Std_ID_282', 'Std_ID_283', 'Std_ID_287', 'Std_ID_288', 'Std_ID_290', 'Std_ID_296', 'Std_ID_298', 'Std_ID_299', 'Std_ID_300', 'Std_ID_301', 'Std_ID_302', 'Std_ID_305', 'Std_ID_310', 'Std_ID_316', 'Std_ID_317', 'Std_ID_320', 'Std_ID_329', 'Std_ID_330', 'Std_ID_331', 'Std_ID_333', 'Std_ID_335', 'Std_ID_336', 'Std_ID_337', 'Std_ID_340', 'Std_ID_345', 'Std_ID_346', 'Std_ID_348', 'Std_ID_349', 'Std_ID_352', 'Std_ID_354', 'Std_ID_355', 'Std_ID_356', 'Std_ID_358', 'Std_ID_361', 'Std_ID_366', 'Std_ID_367', 'Std_ID_373', 'Std_ID_375', 'Std_ID_377', 'Std_ID_378', 'Std_ID_380', 'Std_ID_381', 'Std_ID_383', 'Std_ID_386', 'Std_ID_390', 'Std_ID_393', 'Std_ID_395', 'Std_ID_396', 'Std_ID_398', 'Std_ID_399'] + +MATH105 +['Std_ID_203', 'Std_ID_207', 'Std_ID_213', 'Std_ID_217', 'Std_ID_220', 'Std_ID_232', 'Std_ID_233', 'Std_ID_239', 'Std_ID_246', 'Std_ID_252', 'Std_ID_253', 'Std_ID_254', 'Std_ID_255', 'Std_ID_260', 'Std_ID_263', 'Std_ID_268', 'Std_ID_272', 'Std_ID_275', 'Std_ID_276', 'Std_ID_277', 'Std_ID_281', 'Std_ID_286', 'Std_ID_293', 'Std_ID_308', 'Std_ID_313', 'Std_ID_314', 'Std_ID_315', 'Std_ID_316', 'Std_ID_319', 'Std_ID_320', 'Std_ID_340', 'Std_ID_342', 'Std_ID_343', 'Std_ID_344', 'Std_ID_348', 'Std_ID_350', 'Std_ID_352', 'Std_ID_358', 'Std_ID_359', 'Std_ID_364', 'Std_ID_365', 'Std_ID_381', 'Std_ID_382', 'Std_ID_383', 'Std_ID_388', 'Std_ID_390', 'Std_ID_391', 'Std_ID_397', 'Std_ID_399'] + +MATH106 +['Std_ID_201', 'Std_ID_204', 'Std_ID_208', 'Std_ID_209', 'Std_ID_211', 'Std_ID_213', 'Std_ID_215', 'Std_ID_218', 'Std_ID_224', 'Std_ID_226', 'Std_ID_227', 'Std_ID_234', 'Std_ID_236', 'Std_ID_238', 'Std_ID_241', 'Std_ID_242', 'Std_ID_243', 'Std_ID_244', 'Std_ID_245', 'Std_ID_246', 'Std_ID_249', 'Std_ID_250', 'Std_ID_251', 'Std_ID_252', 'Std_ID_256', 'Std_ID_258', 'Std_ID_259', 'Std_ID_262', 'Std_ID_263', 'Std_ID_265', 'Std_ID_266', 'Std_ID_269', 'Std_ID_270', 'Std_ID_271', 'Std_ID_273', 'Std_ID_278', 'Std_ID_283', 'Std_ID_286', 'Std_ID_294', 'Std_ID_295', 'Std_ID_297', 'Std_ID_300', 'Std_ID_306', 'Std_ID_311', 'Std_ID_313', 'Std_ID_314', 'Std_ID_317', 'Std_ID_319', 'Std_ID_320', 'Std_ID_323', 'Std_ID_324', 'Std_ID_325', 'Std_ID_329', 'Std_ID_331', 'Std_ID_332', 'Std_ID_334', 'Std_ID_337', 'Std_ID_338', 'Std_ID_340', 'Std_ID_342', 'Std_ID_343', 'Std_ID_345', 'Std_ID_347', 'Std_ID_351', 'Std_ID_354', 'Std_ID_356', 'Std_ID_357', 'Std_ID_358', 'Std_ID_363', 'Std_ID_364', 'Std_ID_365', 'Std_ID_366', 'Std_ID_368', 'Std_ID_369', 'Std_ID_370', 'Std_ID_372', 'Std_ID_373', 'Std_ID_376', 'Std_ID_379', 'Std_ID_386', 'Std_ID_389', 'Std_ID_390', 'Std_ID_392', 'Std_ID_393', 'Std_ID_394', 'Std_ID_397', 'Std_ID_399', 'Std_ID_400'] + +MATH107 +['Std_ID_201', 'Std_ID_202', 'Std_ID_204', 'Std_ID_206', 'Std_ID_213', 'Std_ID_214', 'Std_ID_217', 'Std_ID_222', 'Std_ID_223', 'Std_ID_224', 'Std_ID_225', 'Std_ID_227', 'Std_ID_229', 'Std_ID_232', 'Std_ID_238', 'Std_ID_239', 'Std_ID_240', 'Std_ID_241', 'Std_ID_248', 'Std_ID_253', 'Std_ID_254', 'Std_ID_257', 'Std_ID_261', 'Std_ID_266', 'Std_ID_268', 'Std_ID_269', 'Std_ID_274', 'Std_ID_276', 'Std_ID_277', 'Std_ID_279', 'Std_ID_282', 'Std_ID_286', 'Std_ID_290', 'Std_ID_291', 'Std_ID_292', 'Std_ID_294', 'Std_ID_297', 'Std_ID_303', 'Std_ID_305', 'Std_ID_307', 'Std_ID_323', 'Std_ID_324', 'Std_ID_326', 'Std_ID_330', 'Std_ID_339', 'Std_ID_341', 'Std_ID_342', 'Std_ID_346', 'Std_ID_348', 'Std_ID_351', 'Std_ID_355', 'Std_ID_358', 'Std_ID_359', 'Std_ID_360', 'Std_ID_361', 'Std_ID_365', 'Std_ID_369', 'Std_ID_371', 'Std_ID_372', 'Std_ID_374', 'Std_ID_375', 'Std_ID_378', 'Std_ID_380', 'Std_ID_384', 'Std_ID_386', 'Std_ID_387', 'Std_ID_393', 'Std_ID_394', 'Std_ID_396', 'Std_ID_398'] + +MATH108 +['Std_ID_206', 'Std_ID_210', 'Std_ID_212', 'Std_ID_214', 'Std_ID_217', 'Std_ID_218', 'Std_ID_222', 'Std_ID_223', 'Std_ID_229', 'Std_ID_230', 'Std_ID_245', 'Std_ID_247', 'Std_ID_250', 'Std_ID_251', 'Std_ID_252', 'Std_ID_254', 'Std_ID_256', 'Std_ID_258', 'Std_ID_260', 'Std_ID_261', 'Std_ID_262', 'Std_ID_264', 'Std_ID_265', 'Std_ID_268', 'Std_ID_269', 'Std_ID_270', 'Std_ID_271', 'Std_ID_273', 'Std_ID_274', 'Std_ID_276', 'Std_ID_277', 'Std_ID_278', 'Std_ID_280', 'Std_ID_285', 'Std_ID_287', 'Std_ID_289', 'Std_ID_290', 'Std_ID_291', 'Std_ID_292', 'Std_ID_297', 'Std_ID_298', 'Std_ID_299', 'Std_ID_301', 'Std_ID_302', 'Std_ID_304', 'Std_ID_307', 'Std_ID_310', 'Std_ID_312', 'Std_ID_313', 'Std_ID_314', 'Std_ID_315', 'Std_ID_318', 'Std_ID_319', 'Std_ID_320', 'Std_ID_321', 'Std_ID_322', 'Std_ID_325', 'Std_ID_326', 'Std_ID_327', 'Std_ID_334', 'Std_ID_339', 'Std_ID_342', 'Std_ID_343', 'Std_ID_344', 'Std_ID_348', 'Std_ID_349', 'Std_ID_352', 'Std_ID_353', 'Std_ID_357', 'Std_ID_360', 'Std_ID_362', 'Std_ID_364', 'Std_ID_367', 'Std_ID_368', 'Std_ID_371', 'Std_ID_372', 'Std_ID_373', 'Std_ID_377', 'Std_ID_378', 'Std_ID_379', 'Std_ID_380', 'Std_ID_381', 'Std_ID_382', 'Std_ID_383', 'Std_ID_384', 'Std_ID_385', 'Std_ID_387', 'Std_ID_388', 'Std_ID_390', 'Std_ID_392', 'Std_ID_393', 'Std_ID_394', 'Std_ID_399'] + +MATH109 +['Std_ID_205', 'Std_ID_207', 'Std_ID_208', 'Std_ID_209', 'Std_ID_215', 'Std_ID_216', 'Std_ID_219', 'Std_ID_221', 'Std_ID_222', 'Std_ID_225', 'Std_ID_226', 'Std_ID_229', 'Std_ID_230', 'Std_ID_231', 'Std_ID_232', 'Std_ID_235', 'Std_ID_236', 'Std_ID_241', 'Std_ID_246', 'Std_ID_253', 'Std_ID_254', 'Std_ID_257', 'Std_ID_260', 'Std_ID_267', 'Std_ID_272', 'Std_ID_273', 'Std_ID_275', 'Std_ID_277', 'Std_ID_278', 'Std_ID_282', 'Std_ID_283', 'Std_ID_284', 'Std_ID_289', 'Std_ID_294', 'Std_ID_295', 'Std_ID_296', 'Std_ID_298', 'Std_ID_306', 'Std_ID_308', 'Std_ID_309', 'Std_ID_310', 'Std_ID_312', 'Std_ID_313', 'Std_ID_314', 'Std_ID_322', 'Std_ID_327', 'Std_ID_328', 'Std_ID_334', 'Std_ID_338', 'Std_ID_339', 'Std_ID_341', 'Std_ID_344', 'Std_ID_349', 'Std_ID_350', 'Std_ID_353', 'Std_ID_362', 'Std_ID_377', 'Std_ID_385', 'Std_ID_394', 'Std_ID_395', 'Std_ID_396', 'Std_ID_397', 'Std_ID_400'] + +MATH110 +['Std_ID_202', 'Std_ID_203', 'Std_ID_205', 'Std_ID_207', 'Std_ID_208', 'Std_ID_210', 'Std_ID_216', 'Std_ID_217', 'Std_ID_219', 'Std_ID_220', 'Std_ID_221', 'Std_ID_223', 'Std_ID_228', 'Std_ID_229', 'Std_ID_231', 'Std_ID_237', 'Std_ID_239', 'Std_ID_240', 'Std_ID_241', 'Std_ID_242', 'Std_ID_243', 'Std_ID_244', 'Std_ID_248', 'Std_ID_250', 'Std_ID_252', 'Std_ID_257', 'Std_ID_258', 'Std_ID_259', 'Std_ID_262', 'Std_ID_263', 'Std_ID_268', 'Std_ID_270', 'Std_ID_275', 'Std_ID_279', 'Std_ID_280', 'Std_ID_283', 'Std_ID_284', 'Std_ID_285', 'Std_ID_286', 'Std_ID_287', 'Std_ID_289', 'Std_ID_292', 'Std_ID_295', 'Std_ID_297', 'Std_ID_299', 'Std_ID_300', 'Std_ID_301', 'Std_ID_302', 'Std_ID_303', 'Std_ID_304', 'Std_ID_306', 'Std_ID_308', 'Std_ID_309', 'Std_ID_310', 'Std_ID_311', 'Std_ID_312', 'Std_ID_315', 'Std_ID_317', 'Std_ID_318', 'Std_ID_319', 'Std_ID_322', 'Std_ID_324', 'Std_ID_325', 'Std_ID_327', 'Std_ID_329', 'Std_ID_331', 'Std_ID_333', 'Std_ID_335', 'Std_ID_336', 'Std_ID_341', 'Std_ID_344', 'Std_ID_355', 'Std_ID_356', 'Std_ID_359', 'Std_ID_360', 'Std_ID_363', 'Std_ID_368', 'Std_ID_369', 'Std_ID_370', 'Std_ID_375', 'Std_ID_376', 'Std_ID_379', 'Std_ID_388', 'Std_ID_391', 'Std_ID_396'] + +PHYS101 +['Std_ID_401', 'Std_ID_403', 'Std_ID_404', 'Std_ID_406', 'Std_ID_408', 'Std_ID_409', 'Std_ID_410', 'Std_ID_411', 'Std_ID_412', 'Std_ID_416', 'Std_ID_417', 'Std_ID_419', 'Std_ID_422', 'Std_ID_424', 'Std_ID_425', 'Std_ID_426', 'Std_ID_428', 'Std_ID_433', 'Std_ID_435', 'Std_ID_436', 'Std_ID_437', 'Std_ID_443', 'Std_ID_445', 'Std_ID_447', 'Std_ID_449', 'Std_ID_450', 'Std_ID_452', 'Std_ID_454', 'Std_ID_463', 'Std_ID_464', 'Std_ID_469', 'Std_ID_471', 'Std_ID_474', 'Std_ID_476', 'Std_ID_477', 'Std_ID_478', 'Std_ID_481', 'Std_ID_482', 'Std_ID_484', 'Std_ID_486', 'Std_ID_488', 'Std_ID_491', 'Std_ID_492', 'Std_ID_494', 'Std_ID_495', 'Std_ID_497', 'Std_ID_498', 'Std_ID_499', 'Std_ID_501', 'Std_ID_502', 'Std_ID_503', 'Std_ID_505', 'Std_ID_506', 'Std_ID_508', 'Std_ID_509', 'Std_ID_512', 'Std_ID_513', 'Std_ID_515', 'Std_ID_516', 'Std_ID_517', 'Std_ID_524', 'Std_ID_525', 'Std_ID_526', 'Std_ID_534', 'Std_ID_537', 'Std_ID_538', 'Std_ID_539', 'Std_ID_542', 'Std_ID_545', 'Std_ID_546', 'Std_ID_549', 'Std_ID_550', 'Std_ID_551', 'Std_ID_552', 'Std_ID_553', 'Std_ID_556', 'Std_ID_558', 'Std_ID_559', 'Std_ID_560', 'Std_ID_562', 'Std_ID_567', 'Std_ID_569', 'Std_ID_570', 'Std_ID_571', 'Std_ID_572', 'Std_ID_573', 'Std_ID_574', 'Std_ID_575', 'Std_ID_577', 'Std_ID_581', 'Std_ID_583', 'Std_ID_584', 'Std_ID_586', 'Std_ID_589', 'Std_ID_591', 'Std_ID_594'] + +PHYS102 +['Std_ID_401', 'Std_ID_407', 'Std_ID_408', 'Std_ID_409', 'Std_ID_415', 'Std_ID_421', 'Std_ID_425', 'Std_ID_427', 'Std_ID_431', 'Std_ID_432', 'Std_ID_433', 'Std_ID_436', 'Std_ID_439', 'Std_ID_440', 'Std_ID_441', 'Std_ID_442', 'Std_ID_447', 'Std_ID_448', 'Std_ID_449', 'Std_ID_450', 'Std_ID_453', 'Std_ID_454', 'Std_ID_455', 'Std_ID_458', 'Std_ID_461', 'Std_ID_463', 'Std_ID_464', 'Std_ID_466', 'Std_ID_467', 'Std_ID_468', 'Std_ID_469', 'Std_ID_470', 'Std_ID_471', 'Std_ID_472', 'Std_ID_473', 'Std_ID_474', 'Std_ID_478', 'Std_ID_479', 'Std_ID_480', 'Std_ID_482', 'Std_ID_483', 'Std_ID_484', 'Std_ID_486', 'Std_ID_488', 'Std_ID_491', 'Std_ID_495', 'Std_ID_496', 'Std_ID_497', 'Std_ID_498', 'Std_ID_499', 'Std_ID_502', 'Std_ID_504', 'Std_ID_505', 'Std_ID_506', 'Std_ID_509', 'Std_ID_511', 'Std_ID_515', 'Std_ID_517', 'Std_ID_518', 'Std_ID_519', 'Std_ID_522', 'Std_ID_524', 'Std_ID_525', 'Std_ID_526', 'Std_ID_530', 'Std_ID_533', 'Std_ID_534', 'Std_ID_535', 'Std_ID_537', 'Std_ID_538', 'Std_ID_540', 'Std_ID_541', 'Std_ID_542', 'Std_ID_543', 'Std_ID_546', 'Std_ID_549', 'Std_ID_550', 'Std_ID_551', 'Std_ID_554', 'Std_ID_556', 'Std_ID_558', 'Std_ID_560', 'Std_ID_566', 'Std_ID_567', 'Std_ID_569', 'Std_ID_570', 'Std_ID_571', 'Std_ID_574', 'Std_ID_575', 'Std_ID_577', 'Std_ID_582', 'Std_ID_583', 'Std_ID_584', 'Std_ID_586', 'Std_ID_588', 'Std_ID_589', 'Std_ID_590', 'Std_ID_592', 'Std_ID_594', 'Std_ID_595', 'Std_ID_597', 'Std_ID_598', 'Std_ID_600'] + +PHYS103 +['Std_ID_402', 'Std_ID_404', 'Std_ID_410', 'Std_ID_411', 'Std_ID_414', 'Std_ID_415', 'Std_ID_416', 'Std_ID_421', 'Std_ID_423', 'Std_ID_426', 'Std_ID_429', 'Std_ID_430', 'Std_ID_431', 'Std_ID_432', 'Std_ID_433', 'Std_ID_434', 'Std_ID_436', 'Std_ID_437', 'Std_ID_438', 'Std_ID_440', 'Std_ID_442', 'Std_ID_443', 'Std_ID_444', 'Std_ID_451', 'Std_ID_453', 'Std_ID_455', 'Std_ID_458', 'Std_ID_462', 'Std_ID_465', 'Std_ID_466', 'Std_ID_471', 'Std_ID_475', 'Std_ID_480', 'Std_ID_482', 'Std_ID_483', 'Std_ID_484', 'Std_ID_485', 'Std_ID_487', 'Std_ID_489', 'Std_ID_493', 'Std_ID_498', 'Std_ID_500', 'Std_ID_502', 'Std_ID_503', 'Std_ID_505', 'Std_ID_506', 'Std_ID_507', 'Std_ID_510', 'Std_ID_514', 'Std_ID_519', 'Std_ID_520', 'Std_ID_521', 'Std_ID_527', 'Std_ID_528', 'Std_ID_530', 'Std_ID_535', 'Std_ID_536', 'Std_ID_542', 'Std_ID_543', 'Std_ID_547', 'Std_ID_549', 'Std_ID_552', 'Std_ID_554', 'Std_ID_556', 'Std_ID_558', 'Std_ID_559', 'Std_ID_564', 'Std_ID_565', 'Std_ID_566', 'Std_ID_569', 'Std_ID_577', 'Std_ID_578', 'Std_ID_580', 'Std_ID_583', 'Std_ID_584', 'Std_ID_585', 'Std_ID_589', 'Std_ID_595', 'Std_ID_596', 'Std_ID_597', 'Std_ID_598', 'Std_ID_600'] + +PHYS104 +['Std_ID_402', 'Std_ID_406', 'Std_ID_407', 'Std_ID_409', 'Std_ID_410', 'Std_ID_413', 'Std_ID_418', 'Std_ID_419', 'Std_ID_420', 'Std_ID_421', 'Std_ID_429', 'Std_ID_431', 'Std_ID_432', 'Std_ID_438', 'Std_ID_442', 'Std_ID_443', 'Std_ID_444', 'Std_ID_445', 'Std_ID_447', 'Std_ID_448', 'Std_ID_451', 'Std_ID_453', 'Std_ID_454', 'Std_ID_455', 'Std_ID_460', 'Std_ID_461', 'Std_ID_462', 'Std_ID_465', 'Std_ID_469', 'Std_ID_470', 'Std_ID_473', 'Std_ID_477', 'Std_ID_479', 'Std_ID_481', 'Std_ID_482', 'Std_ID_487', 'Std_ID_492', 'Std_ID_493', 'Std_ID_494', 'Std_ID_496', 'Std_ID_497', 'Std_ID_500', 'Std_ID_501', 'Std_ID_504', 'Std_ID_505', 'Std_ID_508', 'Std_ID_511', 'Std_ID_512', 'Std_ID_513', 'Std_ID_514', 'Std_ID_517', 'Std_ID_520', 'Std_ID_521', 'Std_ID_522', 'Std_ID_523', 'Std_ID_527', 'Std_ID_532', 'Std_ID_533', 'Std_ID_534', 'Std_ID_541', 'Std_ID_545', 'Std_ID_547', 'Std_ID_548', 'Std_ID_553', 'Std_ID_557', 'Std_ID_558', 'Std_ID_562', 'Std_ID_563', 'Std_ID_564', 'Std_ID_571', 'Std_ID_572', 'Std_ID_573', 'Std_ID_576', 'Std_ID_579', 'Std_ID_580', 'Std_ID_582', 'Std_ID_587', 'Std_ID_589', 'Std_ID_590', 'Std_ID_593', 'Std_ID_594', 'Std_ID_595', 'Std_ID_597', 'Std_ID_599'] + +PHYS105 +['Std_ID_402', 'Std_ID_403', 'Std_ID_406', 'Std_ID_407', 'Std_ID_408', 'Std_ID_411', 'Std_ID_412', 'Std_ID_414', 'Std_ID_415', 'Std_ID_417', 'Std_ID_418', 'Std_ID_422', 'Std_ID_426', 'Std_ID_427', 'Std_ID_428', 'Std_ID_429', 'Std_ID_434', 'Std_ID_435', 'Std_ID_438', 'Std_ID_439', 'Std_ID_442', 'Std_ID_446', 'Std_ID_451', 'Std_ID_456', 'Std_ID_457', 'Std_ID_460', 'Std_ID_465', 'Std_ID_472', 'Std_ID_477', 'Std_ID_483', 'Std_ID_484', 'Std_ID_485', 'Std_ID_487', 'Std_ID_490', 'Std_ID_492', 'Std_ID_493', 'Std_ID_497', 'Std_ID_500', 'Std_ID_503', 'Std_ID_507', 'Std_ID_508', 'Std_ID_510', 'Std_ID_511', 'Std_ID_516', 'Std_ID_518', 'Std_ID_522', 'Std_ID_523', 'Std_ID_526', 'Std_ID_527', 'Std_ID_531', 'Std_ID_532', 'Std_ID_536', 'Std_ID_544', 'Std_ID_545', 'Std_ID_555', 'Std_ID_556', 'Std_ID_557', 'Std_ID_561', 'Std_ID_562', 'Std_ID_563', 'Std_ID_564', 'Std_ID_565', 'Std_ID_569', 'Std_ID_576', 'Std_ID_579', 'Std_ID_585', 'Std_ID_587', 'Std_ID_591', 'Std_ID_593', 'Std_ID_598', 'Std_ID_599'] + +PHYS106 +['Std_ID_403', 'Std_ID_404', 'Std_ID_405', 'Std_ID_406', 'Std_ID_420', 'Std_ID_422', 'Std_ID_424', 'Std_ID_427', 'Std_ID_434', 'Std_ID_435', 'Std_ID_438', 'Std_ID_443', 'Std_ID_444', 'Std_ID_446', 'Std_ID_450', 'Std_ID_452', 'Std_ID_454', 'Std_ID_456', 'Std_ID_457', 'Std_ID_458', 'Std_ID_459', 'Std_ID_460', 'Std_ID_461', 'Std_ID_462', 'Std_ID_465', 'Std_ID_466', 'Std_ID_472', 'Std_ID_474', 'Std_ID_477', 'Std_ID_480', 'Std_ID_481', 'Std_ID_491', 'Std_ID_493', 'Std_ID_499', 'Std_ID_502', 'Std_ID_504', 'Std_ID_510', 'Std_ID_513', 'Std_ID_514', 'Std_ID_515', 'Std_ID_516', 'Std_ID_517', 'Std_ID_519', 'Std_ID_524', 'Std_ID_526', 'Std_ID_528', 'Std_ID_530', 'Std_ID_531', 'Std_ID_532', 'Std_ID_539', 'Std_ID_544', 'Std_ID_546', 'Std_ID_547', 'Std_ID_548', 'Std_ID_551', 'Std_ID_552', 'Std_ID_554', 'Std_ID_555', 'Std_ID_557', 'Std_ID_560', 'Std_ID_561', 'Std_ID_563', 'Std_ID_564', 'Std_ID_568', 'Std_ID_572', 'Std_ID_573', 'Std_ID_575', 'Std_ID_576', 'Std_ID_578', 'Std_ID_579', 'Std_ID_580', 'Std_ID_587', 'Std_ID_592', 'Std_ID_599'] + +PHYS107 +['Std_ID_405', 'Std_ID_407', 'Std_ID_410', 'Std_ID_412', 'Std_ID_413', 'Std_ID_414', 'Std_ID_417', 'Std_ID_418', 'Std_ID_423', 'Std_ID_425', 'Std_ID_428', 'Std_ID_429', 'Std_ID_430', 'Std_ID_436', 'Std_ID_437', 'Std_ID_439', 'Std_ID_441', 'Std_ID_444', 'Std_ID_451', 'Std_ID_452', 'Std_ID_456', 'Std_ID_457', 'Std_ID_459', 'Std_ID_461', 'Std_ID_463', 'Std_ID_464', 'Std_ID_467', 'Std_ID_468', 'Std_ID_475', 'Std_ID_476', 'Std_ID_479', 'Std_ID_487', 'Std_ID_488', 'Std_ID_489', 'Std_ID_490', 'Std_ID_495', 'Std_ID_496', 'Std_ID_507', 'Std_ID_512', 'Std_ID_513', 'Std_ID_520', 'Std_ID_522', 'Std_ID_527', 'Std_ID_529', 'Std_ID_531', 'Std_ID_533', 'Std_ID_540', 'Std_ID_543', 'Std_ID_547', 'Std_ID_560', 'Std_ID_562', 'Std_ID_567', 'Std_ID_568', 'Std_ID_570', 'Std_ID_582', 'Std_ID_585', 'Std_ID_591', 'Std_ID_592', 'Std_ID_593', 'Std_ID_595', 'Std_ID_600'] + +PHYS108 +['Std_ID_401', 'Std_ID_402', 'Std_ID_403', 'Std_ID_404', 'Std_ID_405', 'Std_ID_408', 'Std_ID_415', 'Std_ID_416', 'Std_ID_418', 'Std_ID_419', 'Std_ID_420', 'Std_ID_421', 'Std_ID_422', 'Std_ID_424', 'Std_ID_426', 'Std_ID_430', 'Std_ID_434', 'Std_ID_441', 'Std_ID_445', 'Std_ID_446', 'Std_ID_448', 'Std_ID_449', 'Std_ID_450', 'Std_ID_453', 'Std_ID_455', 'Std_ID_457', 'Std_ID_458', 'Std_ID_459', 'Std_ID_460', 'Std_ID_466', 'Std_ID_467', 'Std_ID_468', 'Std_ID_469', 'Std_ID_475', 'Std_ID_480', 'Std_ID_481', 'Std_ID_485', 'Std_ID_486', 'Std_ID_489', 'Std_ID_490', 'Std_ID_491', 'Std_ID_495', 'Std_ID_496', 'Std_ID_498', 'Std_ID_499', 'Std_ID_500', 'Std_ID_501', 'Std_ID_504', 'Std_ID_506', 'Std_ID_507', 'Std_ID_508', 'Std_ID_509', 'Std_ID_511', 'Std_ID_512', 'Std_ID_514', 'Std_ID_515', 'Std_ID_516', 'Std_ID_518', 'Std_ID_520', 'Std_ID_521', 'Std_ID_523', 'Std_ID_524', 'Std_ID_525', 'Std_ID_528', 'Std_ID_529', 'Std_ID_530', 'Std_ID_531', 'Std_ID_532', 'Std_ID_533', 'Std_ID_535', 'Std_ID_536', 'Std_ID_537', 'Std_ID_539', 'Std_ID_540', 'Std_ID_542', 'Std_ID_543', 'Std_ID_544', 'Std_ID_546', 'Std_ID_551', 'Std_ID_552', 'Std_ID_555', 'Std_ID_559', 'Std_ID_561', 'Std_ID_565', 'Std_ID_567', 'Std_ID_568', 'Std_ID_570', 'Std_ID_571', 'Std_ID_575', 'Std_ID_577', 'Std_ID_580', 'Std_ID_581', 'Std_ID_582', 'Std_ID_584', 'Std_ID_586', 'Std_ID_588', 'Std_ID_594', 'Std_ID_596', 'Std_ID_600'] + +PHYS109 +['Std_ID_405', 'Std_ID_409', 'Std_ID_413', 'Std_ID_417', 'Std_ID_423', 'Std_ID_424', 'Std_ID_427', 'Std_ID_428', 'Std_ID_431', 'Std_ID_432', 'Std_ID_433', 'Std_ID_435', 'Std_ID_437', 'Std_ID_439', 'Std_ID_440', 'Std_ID_445', 'Std_ID_447', 'Std_ID_448', 'Std_ID_449', 'Std_ID_452', 'Std_ID_456', 'Std_ID_459', 'Std_ID_463', 'Std_ID_467', 'Std_ID_468', 'Std_ID_470', 'Std_ID_471', 'Std_ID_472', 'Std_ID_473', 'Std_ID_475', 'Std_ID_476', 'Std_ID_478', 'Std_ID_485', 'Std_ID_490', 'Std_ID_494', 'Std_ID_501', 'Std_ID_503', 'Std_ID_510', 'Std_ID_521', 'Std_ID_528', 'Std_ID_529', 'Std_ID_536', 'Std_ID_537', 'Std_ID_538', 'Std_ID_540', 'Std_ID_541', 'Std_ID_544', 'Std_ID_545', 'Std_ID_548', 'Std_ID_549', 'Std_ID_550', 'Std_ID_553', 'Std_ID_557', 'Std_ID_566', 'Std_ID_568', 'Std_ID_572', 'Std_ID_574', 'Std_ID_578', 'Std_ID_581', 'Std_ID_583', 'Std_ID_586', 'Std_ID_587', 'Std_ID_588', 'Std_ID_590', 'Std_ID_592', 'Std_ID_596', 'Std_ID_598', 'Std_ID_599'] + +PHYS110 +['Std_ID_401', 'Std_ID_411', 'Std_ID_412', 'Std_ID_413', 'Std_ID_414', 'Std_ID_416', 'Std_ID_419', 'Std_ID_420', 'Std_ID_423', 'Std_ID_425', 'Std_ID_430', 'Std_ID_440', 'Std_ID_441', 'Std_ID_446', 'Std_ID_462', 'Std_ID_464', 'Std_ID_470', 'Std_ID_473', 'Std_ID_474', 'Std_ID_476', 'Std_ID_478', 'Std_ID_479', 'Std_ID_483', 'Std_ID_486', 'Std_ID_488', 'Std_ID_489', 'Std_ID_492', 'Std_ID_494', 'Std_ID_509', 'Std_ID_518', 'Std_ID_519', 'Std_ID_523', 'Std_ID_525', 'Std_ID_529', 'Std_ID_534', 'Std_ID_535', 'Std_ID_538', 'Std_ID_539', 'Std_ID_541', 'Std_ID_548', 'Std_ID_550', 'Std_ID_553', 'Std_ID_554', 'Std_ID_555', 'Std_ID_559', 'Std_ID_561', 'Std_ID_563', 'Std_ID_565', 'Std_ID_566', 'Std_ID_573', 'Std_ID_574', 'Std_ID_576', 'Std_ID_578', 'Std_ID_579', 'Std_ID_581', 'Std_ID_585', 'Std_ID_588', 'Std_ID_590', 'Std_ID_591', 'Std_ID_593', 'Std_ID_596', 'Std_ID_597'] + +CHEM101 +['Std_ID_605', 'Std_ID_607', 'Std_ID_611', 'Std_ID_613', 'Std_ID_615', 'Std_ID_616', 'Std_ID_620', 'Std_ID_628', 'Std_ID_629', 'Std_ID_630', 'Std_ID_631', 'Std_ID_636', 'Std_ID_638', 'Std_ID_644', 'Std_ID_649', 'Std_ID_651', 'Std_ID_652', 'Std_ID_653', 'Std_ID_654', 'Std_ID_655', 'Std_ID_656', 'Std_ID_657', 'Std_ID_659', 'Std_ID_661', 'Std_ID_663', 'Std_ID_664', 'Std_ID_666', 'Std_ID_669', 'Std_ID_672', 'Std_ID_673', 'Std_ID_674', 'Std_ID_675', 'Std_ID_676', 'Std_ID_678', 'Std_ID_682', 'Std_ID_683', 'Std_ID_684', 'Std_ID_685', 'Std_ID_686', 'Std_ID_687', 'Std_ID_688', 'Std_ID_693', 'Std_ID_699', 'Std_ID_703', 'Std_ID_704', 'Std_ID_710', 'Std_ID_711', 'Std_ID_712', 'Std_ID_714', 'Std_ID_719', 'Std_ID_722', 'Std_ID_723', 'Std_ID_725', 'Std_ID_727', 'Std_ID_732', 'Std_ID_734', 'Std_ID_736', 'Std_ID_738', 'Std_ID_740', 'Std_ID_741', 'Std_ID_742', 'Std_ID_744', 'Std_ID_745', 'Std_ID_747', 'Std_ID_749', 'Std_ID_750', 'Std_ID_754', 'Std_ID_755', 'Std_ID_763', 'Std_ID_767', 'Std_ID_770', 'Std_ID_771', 'Std_ID_772', 'Std_ID_773', 'Std_ID_776', 'Std_ID_777', 'Std_ID_778', 'Std_ID_780', 'Std_ID_782', 'Std_ID_783', 'Std_ID_786', 'Std_ID_789', 'Std_ID_791', 'Std_ID_794', 'Std_ID_795', 'Std_ID_797', 'Std_ID_799', 'Std_ID_800'] + +CHEM102 +['Std_ID_605', 'Std_ID_607', 'Std_ID_609', 'Std_ID_613', 'Std_ID_614', 'Std_ID_617', 'Std_ID_619', 'Std_ID_620', 'Std_ID_621', 'Std_ID_622', 'Std_ID_625', 'Std_ID_627', 'Std_ID_629', 'Std_ID_631', 'Std_ID_632', 'Std_ID_633', 'Std_ID_635', 'Std_ID_637', 'Std_ID_640', 'Std_ID_648', 'Std_ID_651', 'Std_ID_652', 'Std_ID_653', 'Std_ID_655', 'Std_ID_657', 'Std_ID_660', 'Std_ID_661', 'Std_ID_663', 'Std_ID_664', 'Std_ID_665', 'Std_ID_669', 'Std_ID_672', 'Std_ID_673', 'Std_ID_675', 'Std_ID_680', 'Std_ID_682', 'Std_ID_683', 'Std_ID_684', 'Std_ID_687', 'Std_ID_690', 'Std_ID_691', 'Std_ID_693', 'Std_ID_695', 'Std_ID_697', 'Std_ID_698', 'Std_ID_699', 'Std_ID_700', 'Std_ID_703', 'Std_ID_704', 'Std_ID_706', 'Std_ID_710', 'Std_ID_711', 'Std_ID_713', 'Std_ID_714', 'Std_ID_717', 'Std_ID_718', 'Std_ID_719', 'Std_ID_721', 'Std_ID_722', 'Std_ID_723', 'Std_ID_724', 'Std_ID_725', 'Std_ID_728', 'Std_ID_729', 'Std_ID_731', 'Std_ID_732', 'Std_ID_734', 'Std_ID_740', 'Std_ID_741', 'Std_ID_742', 'Std_ID_744', 'Std_ID_749', 'Std_ID_753', 'Std_ID_754', 'Std_ID_755', 'Std_ID_757', 'Std_ID_759', 'Std_ID_761', 'Std_ID_763', 'Std_ID_766', 'Std_ID_767', 'Std_ID_769', 'Std_ID_770', 'Std_ID_771', 'Std_ID_772', 'Std_ID_777', 'Std_ID_778', 'Std_ID_780', 'Std_ID_783', 'Std_ID_784', 'Std_ID_785', 'Std_ID_791', 'Std_ID_792', 'Std_ID_794', 'Std_ID_797', 'Std_ID_799', 'Std_ID_800'] + +CHEM103 +['Std_ID_603', 'Std_ID_606', 'Std_ID_611', 'Std_ID_617', 'Std_ID_618', 'Std_ID_623', 'Std_ID_625', 'Std_ID_626', 'Std_ID_629', 'Std_ID_630', 'Std_ID_631', 'Std_ID_632', 'Std_ID_634', 'Std_ID_635', 'Std_ID_636', 'Std_ID_641', 'Std_ID_643', 'Std_ID_644', 'Std_ID_647', 'Std_ID_648', 'Std_ID_649', 'Std_ID_655', 'Std_ID_658', 'Std_ID_659', 'Std_ID_660', 'Std_ID_662', 'Std_ID_668', 'Std_ID_669', 'Std_ID_670', 'Std_ID_674', 'Std_ID_679', 'Std_ID_680', 'Std_ID_681', 'Std_ID_683', 'Std_ID_691', 'Std_ID_694', 'Std_ID_695', 'Std_ID_696', 'Std_ID_697', 'Std_ID_698', 'Std_ID_700', 'Std_ID_702', 'Std_ID_705', 'Std_ID_706', 'Std_ID_709', 'Std_ID_713', 'Std_ID_714', 'Std_ID_715', 'Std_ID_716', 'Std_ID_718', 'Std_ID_720', 'Std_ID_722', 'Std_ID_723', 'Std_ID_724', 'Std_ID_726', 'Std_ID_728', 'Std_ID_729', 'Std_ID_733', 'Std_ID_735', 'Std_ID_737', 'Std_ID_744', 'Std_ID_751', 'Std_ID_752', 'Std_ID_754', 'Std_ID_757', 'Std_ID_758', 'Std_ID_761', 'Std_ID_764', 'Std_ID_766', 'Std_ID_768', 'Std_ID_771', 'Std_ID_774', 'Std_ID_783', 'Std_ID_784', 'Std_ID_788', 'Std_ID_791', 'Std_ID_792', 'Std_ID_793', 'Std_ID_795', 'Std_ID_796'] + +CHEM104 +['Std_ID_603', 'Std_ID_604', 'Std_ID_606', 'Std_ID_607', 'Std_ID_609', 'Std_ID_610', 'Std_ID_612', 'Std_ID_613', 'Std_ID_614', 'Std_ID_615', 'Std_ID_617', 'Std_ID_618', 'Std_ID_621', 'Std_ID_622', 'Std_ID_623', 'Std_ID_626', 'Std_ID_628', 'Std_ID_629', 'Std_ID_630', 'Std_ID_633', 'Std_ID_634', 'Std_ID_635', 'Std_ID_640', 'Std_ID_641', 'Std_ID_642', 'Std_ID_643', 'Std_ID_644', 'Std_ID_645', 'Std_ID_646', 'Std_ID_650', 'Std_ID_652', 'Std_ID_653', 'Std_ID_654', 'Std_ID_662', 'Std_ID_665', 'Std_ID_669', 'Std_ID_670', 'Std_ID_671', 'Std_ID_677', 'Std_ID_680', 'Std_ID_681', 'Std_ID_682', 'Std_ID_689', 'Std_ID_691', 'Std_ID_692', 'Std_ID_694', 'Std_ID_697', 'Std_ID_700', 'Std_ID_701', 'Std_ID_702', 'Std_ID_705', 'Std_ID_715', 'Std_ID_717', 'Std_ID_726', 'Std_ID_727', 'Std_ID_728', 'Std_ID_729', 'Std_ID_730', 'Std_ID_733', 'Std_ID_735', 'Std_ID_737', 'Std_ID_738', 'Std_ID_739', 'Std_ID_743', 'Std_ID_746', 'Std_ID_748', 'Std_ID_751', 'Std_ID_752', 'Std_ID_753', 'Std_ID_754', 'Std_ID_759', 'Std_ID_760', 'Std_ID_761', 'Std_ID_762', 'Std_ID_764', 'Std_ID_765', 'Std_ID_766', 'Std_ID_767', 'Std_ID_768', 'Std_ID_773', 'Std_ID_774', 'Std_ID_775', 'Std_ID_779', 'Std_ID_782', 'Std_ID_784', 'Std_ID_785', 'Std_ID_789', 'Std_ID_790', 'Std_ID_791', 'Std_ID_792', 'Std_ID_795', 'Std_ID_796', 'Std_ID_797', 'Std_ID_798'] + +CHEM105 +['Std_ID_602', 'Std_ID_606', 'Std_ID_607', 'Std_ID_612', 'Std_ID_613', 'Std_ID_615', 'Std_ID_616', 'Std_ID_620', 'Std_ID_621', 'Std_ID_624', 'Std_ID_625', 'Std_ID_626', 'Std_ID_632', 'Std_ID_633', 'Std_ID_634', 'Std_ID_639', 'Std_ID_640', 'Std_ID_641', 'Std_ID_643', 'Std_ID_646', 'Std_ID_656', 'Std_ID_657', 'Std_ID_658', 'Std_ID_659', 'Std_ID_668', 'Std_ID_671', 'Std_ID_677', 'Std_ID_678', 'Std_ID_688', 'Std_ID_689', 'Std_ID_694', 'Std_ID_697', 'Std_ID_707', 'Std_ID_709', 'Std_ID_710', 'Std_ID_712', 'Std_ID_716', 'Std_ID_719', 'Std_ID_720', 'Std_ID_721', 'Std_ID_725', 'Std_ID_739', 'Std_ID_740', 'Std_ID_743', 'Std_ID_748', 'Std_ID_756', 'Std_ID_757', 'Std_ID_762', 'Std_ID_763', 'Std_ID_765', 'Std_ID_767', 'Std_ID_772', 'Std_ID_774', 'Std_ID_775', 'Std_ID_776', 'Std_ID_781', 'Std_ID_784', 'Std_ID_785', 'Std_ID_787', 'Std_ID_788', 'Std_ID_790', 'Std_ID_793', 'Std_ID_798'] + +CHEM106 +['Std_ID_601', 'Std_ID_602', 'Std_ID_603', 'Std_ID_604', 'Std_ID_608', 'Std_ID_609', 'Std_ID_610', 'Std_ID_611', 'Std_ID_612', 'Std_ID_614', 'Std_ID_616', 'Std_ID_617', 'Std_ID_619', 'Std_ID_621', 'Std_ID_624', 'Std_ID_626', 'Std_ID_630', 'Std_ID_634', 'Std_ID_637', 'Std_ID_638', 'Std_ID_640', 'Std_ID_642', 'Std_ID_645', 'Std_ID_647', 'Std_ID_648', 'Std_ID_652', 'Std_ID_654', 'Std_ID_656', 'Std_ID_658', 'Std_ID_659', 'Std_ID_666', 'Std_ID_670', 'Std_ID_673', 'Std_ID_674', 'Std_ID_675', 'Std_ID_678', 'Std_ID_679', 'Std_ID_684', 'Std_ID_685', 'Std_ID_690', 'Std_ID_693', 'Std_ID_696', 'Std_ID_701', 'Std_ID_703', 'Std_ID_707', 'Std_ID_708', 'Std_ID_709', 'Std_ID_718', 'Std_ID_721', 'Std_ID_723', 'Std_ID_724', 'Std_ID_727', 'Std_ID_729', 'Std_ID_730', 'Std_ID_732', 'Std_ID_739', 'Std_ID_741', 'Std_ID_745', 'Std_ID_746', 'Std_ID_751', 'Std_ID_756', 'Std_ID_759', 'Std_ID_760', 'Std_ID_761', 'Std_ID_762', 'Std_ID_763', 'Std_ID_768', 'Std_ID_769', 'Std_ID_770', 'Std_ID_775', 'Std_ID_779', 'Std_ID_780', 'Std_ID_783', 'Std_ID_785', 'Std_ID_786', 'Std_ID_790', 'Std_ID_793', 'Std_ID_797'] + +CHEM107 +['Std_ID_601', 'Std_ID_602', 'Std_ID_603', 'Std_ID_605', 'Std_ID_608', 'Std_ID_609', 'Std_ID_610', 'Std_ID_612', 'Std_ID_616', 'Std_ID_623', 'Std_ID_624', 'Std_ID_627', 'Std_ID_628', 'Std_ID_637', 'Std_ID_639', 'Std_ID_646', 'Std_ID_649', 'Std_ID_650', 'Std_ID_660', 'Std_ID_663', 'Std_ID_667', 'Std_ID_673', 'Std_ID_674', 'Std_ID_677', 'Std_ID_678', 'Std_ID_679', 'Std_ID_681', 'Std_ID_685', 'Std_ID_686', 'Std_ID_688', 'Std_ID_689', 'Std_ID_692', 'Std_ID_693', 'Std_ID_699', 'Std_ID_701', 'Std_ID_704', 'Std_ID_706', 'Std_ID_707', 'Std_ID_708', 'Std_ID_712', 'Std_ID_713', 'Std_ID_715', 'Std_ID_716', 'Std_ID_717', 'Std_ID_719', 'Std_ID_720', 'Std_ID_722', 'Std_ID_731', 'Std_ID_734', 'Std_ID_735', 'Std_ID_736', 'Std_ID_738', 'Std_ID_745', 'Std_ID_746', 'Std_ID_747', 'Std_ID_748', 'Std_ID_751', 'Std_ID_752', 'Std_ID_756', 'Std_ID_759', 'Std_ID_760', 'Std_ID_765', 'Std_ID_768', 'Std_ID_769', 'Std_ID_772', 'Std_ID_773', 'Std_ID_776', 'Std_ID_777', 'Std_ID_778', 'Std_ID_779', 'Std_ID_780', 'Std_ID_781', 'Std_ID_782', 'Std_ID_795', 'Std_ID_798', 'Std_ID_799'] + +CHEM108 +['Std_ID_601', 'Std_ID_608', 'Std_ID_619', 'Std_ID_622', 'Std_ID_624', 'Std_ID_625', 'Std_ID_627', 'Std_ID_632', 'Std_ID_635', 'Std_ID_636', 'Std_ID_638', 'Std_ID_641', 'Std_ID_643', 'Std_ID_645', 'Std_ID_651', 'Std_ID_654', 'Std_ID_656', 'Std_ID_661', 'Std_ID_662', 'Std_ID_664', 'Std_ID_666', 'Std_ID_667', 'Std_ID_668', 'Std_ID_670', 'Std_ID_671', 'Std_ID_672', 'Std_ID_676', 'Std_ID_679', 'Std_ID_688', 'Std_ID_689', 'Std_ID_690', 'Std_ID_691', 'Std_ID_692', 'Std_ID_698', 'Std_ID_700', 'Std_ID_703', 'Std_ID_704', 'Std_ID_705', 'Std_ID_708', 'Std_ID_711', 'Std_ID_712', 'Std_ID_721', 'Std_ID_726', 'Std_ID_731', 'Std_ID_736', 'Std_ID_738', 'Std_ID_743', 'Std_ID_744', 'Std_ID_747', 'Std_ID_750', 'Std_ID_753', 'Std_ID_755', 'Std_ID_756', 'Std_ID_757', 'Std_ID_758', 'Std_ID_762', 'Std_ID_764', 'Std_ID_770', 'Std_ID_778', 'Std_ID_781', 'Std_ID_787', 'Std_ID_788', 'Std_ID_792', 'Std_ID_794', 'Std_ID_796'] + +CHEM109 +['Std_ID_601', 'Std_ID_602', 'Std_ID_604', 'Std_ID_605', 'Std_ID_608', 'Std_ID_614', 'Std_ID_618', 'Std_ID_623', 'Std_ID_631', 'Std_ID_633', 'Std_ID_637', 'Std_ID_639', 'Std_ID_642', 'Std_ID_644', 'Std_ID_647', 'Std_ID_648', 'Std_ID_649', 'Std_ID_650', 'Std_ID_653', 'Std_ID_661', 'Std_ID_662', 'Std_ID_665', 'Std_ID_666', 'Std_ID_667', 'Std_ID_671', 'Std_ID_676', 'Std_ID_680', 'Std_ID_682', 'Std_ID_684', 'Std_ID_686', 'Std_ID_687', 'Std_ID_690', 'Std_ID_692', 'Std_ID_694', 'Std_ID_695', 'Std_ID_696', 'Std_ID_698', 'Std_ID_702', 'Std_ID_705', 'Std_ID_706', 'Std_ID_707', 'Std_ID_709', 'Std_ID_710', 'Std_ID_713', 'Std_ID_714', 'Std_ID_715', 'Std_ID_716', 'Std_ID_717', 'Std_ID_718', 'Std_ID_727', 'Std_ID_730', 'Std_ID_731', 'Std_ID_732', 'Std_ID_733', 'Std_ID_736', 'Std_ID_737', 'Std_ID_739', 'Std_ID_741', 'Std_ID_742', 'Std_ID_743', 'Std_ID_745', 'Std_ID_749', 'Std_ID_750', 'Std_ID_752', 'Std_ID_758', 'Std_ID_760', 'Std_ID_764', 'Std_ID_765', 'Std_ID_769', 'Std_ID_771', 'Std_ID_775', 'Std_ID_777', 'Std_ID_786', 'Std_ID_787', 'Std_ID_789', 'Std_ID_796', 'Std_ID_798', 'Std_ID_799', 'Std_ID_800'] + +CHEM110 +['Std_ID_604', 'Std_ID_606', 'Std_ID_610', 'Std_ID_611', 'Std_ID_615', 'Std_ID_618', 'Std_ID_619', 'Std_ID_620', 'Std_ID_622', 'Std_ID_627', 'Std_ID_628', 'Std_ID_636', 'Std_ID_638', 'Std_ID_639', 'Std_ID_642', 'Std_ID_645', 'Std_ID_646', 'Std_ID_647', 'Std_ID_650', 'Std_ID_651', 'Std_ID_655', 'Std_ID_657', 'Std_ID_658', 'Std_ID_660', 'Std_ID_663', 'Std_ID_664', 'Std_ID_665', 'Std_ID_667', 'Std_ID_668', 'Std_ID_672', 'Std_ID_675', 'Std_ID_676', 'Std_ID_677', 'Std_ID_681', 'Std_ID_683', 'Std_ID_685', 'Std_ID_686', 'Std_ID_687', 'Std_ID_695', 'Std_ID_696', 'Std_ID_699', 'Std_ID_701', 'Std_ID_702', 'Std_ID_708', 'Std_ID_711', 'Std_ID_720', 'Std_ID_724', 'Std_ID_725', 'Std_ID_726', 'Std_ID_728', 'Std_ID_730', 'Std_ID_733', 'Std_ID_734', 'Std_ID_735', 'Std_ID_737', 'Std_ID_740', 'Std_ID_742', 'Std_ID_746', 'Std_ID_747', 'Std_ID_748', 'Std_ID_749', 'Std_ID_750', 'Std_ID_753', 'Std_ID_755', 'Std_ID_758', 'Std_ID_766', 'Std_ID_773', 'Std_ID_774', 'Std_ID_776', 'Std_ID_779', 'Std_ID_781', 'Std_ID_782', 'Std_ID_786', 'Std_ID_787', 'Std_ID_788', 'Std_ID_789', 'Std_ID_790', 'Std_ID_793', 'Std_ID_794', 'Std_ID_800'] + +BIO101 +['Std_ID_801', 'Std_ID_804', 'Std_ID_805', 'Std_ID_806', 'Std_ID_807', 'Std_ID_809', 'Std_ID_810', 'Std_ID_811', 'Std_ID_812', 'Std_ID_816', 'Std_ID_817', 'Std_ID_822', 'Std_ID_825', 'Std_ID_835', 'Std_ID_837', 'Std_ID_839', 'Std_ID_841', 'Std_ID_842', 'Std_ID_848', 'Std_ID_849', 'Std_ID_850', 'Std_ID_851', 'Std_ID_856', 'Std_ID_857', 'Std_ID_858', 'Std_ID_859', 'Std_ID_860', 'Std_ID_862', 'Std_ID_863', 'Std_ID_867', 'Std_ID_869', 'Std_ID_871', 'Std_ID_872', 'Std_ID_873', 'Std_ID_874', 'Std_ID_877', 'Std_ID_880', 'Std_ID_885', 'Std_ID_887', 'Std_ID_888', 'Std_ID_889', 'Std_ID_890', 'Std_ID_891', 'Std_ID_896', 'Std_ID_897', 'Std_ID_903', 'Std_ID_904', 'Std_ID_906', 'Std_ID_907', 'Std_ID_908', 'Std_ID_909', 'Std_ID_911', 'Std_ID_912', 'Std_ID_913', 'Std_ID_914', 'Std_ID_918', 'Std_ID_920', 'Std_ID_921', 'Std_ID_922', 'Std_ID_923', 'Std_ID_924', 'Std_ID_925', 'Std_ID_926', 'Std_ID_931', 'Std_ID_932', 'Std_ID_934', 'Std_ID_936', 'Std_ID_938', 'Std_ID_941', 'Std_ID_942', 'Std_ID_944', 'Std_ID_946', 'Std_ID_948', 'Std_ID_949', 'Std_ID_951', 'Std_ID_954', 'Std_ID_959', 'Std_ID_960', 'Std_ID_962', 'Std_ID_963', 'Std_ID_964', 'Std_ID_969', 'Std_ID_975', 'Std_ID_976', 'Std_ID_978', 'Std_ID_979', 'Std_ID_982', 'Std_ID_983', 'Std_ID_985', 'Std_ID_990', 'Std_ID_991', 'Std_ID_993', 'Std_ID_996', 'Std_ID_998', 'Std_ID_1000'] + +BIO102 +['Std_ID_802', 'Std_ID_807', 'Std_ID_809', 'Std_ID_811', 'Std_ID_812', 'Std_ID_818', 'Std_ID_819', 'Std_ID_822', 'Std_ID_830', 'Std_ID_832', 'Std_ID_833', 'Std_ID_835', 'Std_ID_839', 'Std_ID_840', 'Std_ID_841', 'Std_ID_842', 'Std_ID_845', 'Std_ID_848', 'Std_ID_849', 'Std_ID_858', 'Std_ID_865', 'Std_ID_866', 'Std_ID_867', 'Std_ID_868', 'Std_ID_869', 'Std_ID_870', 'Std_ID_871', 'Std_ID_872', 'Std_ID_873', 'Std_ID_878', 'Std_ID_880', 'Std_ID_882', 'Std_ID_883', 'Std_ID_884', 'Std_ID_886', 'Std_ID_887', 'Std_ID_889', 'Std_ID_890', 'Std_ID_891', 'Std_ID_892', 'Std_ID_896', 'Std_ID_898', 'Std_ID_899', 'Std_ID_900', 'Std_ID_903', 'Std_ID_907', 'Std_ID_908', 'Std_ID_914', 'Std_ID_921', 'Std_ID_923', 'Std_ID_934', 'Std_ID_935', 'Std_ID_936', 'Std_ID_938', 'Std_ID_939', 'Std_ID_940', 'Std_ID_941', 'Std_ID_944', 'Std_ID_945', 'Std_ID_948', 'Std_ID_949', 'Std_ID_951', 'Std_ID_954', 'Std_ID_957', 'Std_ID_959', 'Std_ID_960', 'Std_ID_961', 'Std_ID_963', 'Std_ID_965', 'Std_ID_967', 'Std_ID_970', 'Std_ID_971', 'Std_ID_974', 'Std_ID_975', 'Std_ID_976', 'Std_ID_977', 'Std_ID_982', 'Std_ID_987', 'Std_ID_990', 'Std_ID_992', 'Std_ID_993', 'Std_ID_994', 'Std_ID_997', 'Std_ID_998', 'Std_ID_999'] + +BIO103 +['Std_ID_801', 'Std_ID_802', 'Std_ID_803', 'Std_ID_807', 'Std_ID_808', 'Std_ID_810', 'Std_ID_812', 'Std_ID_814', 'Std_ID_818', 'Std_ID_819', 'Std_ID_820', 'Std_ID_821', 'Std_ID_827', 'Std_ID_828', 'Std_ID_830', 'Std_ID_832', 'Std_ID_836', 'Std_ID_838', 'Std_ID_840', 'Std_ID_841', 'Std_ID_842', 'Std_ID_843', 'Std_ID_844', 'Std_ID_845', 'Std_ID_846', 'Std_ID_848', 'Std_ID_850', 'Std_ID_852', 'Std_ID_854', 'Std_ID_855', 'Std_ID_862', 'Std_ID_864', 'Std_ID_865', 'Std_ID_866', 'Std_ID_868', 'Std_ID_870', 'Std_ID_873', 'Std_ID_876', 'Std_ID_878', 'Std_ID_881', 'Std_ID_883', 'Std_ID_884', 'Std_ID_886', 'Std_ID_890', 'Std_ID_892', 'Std_ID_893', 'Std_ID_897', 'Std_ID_898', 'Std_ID_900', 'Std_ID_905', 'Std_ID_908', 'Std_ID_912', 'Std_ID_913', 'Std_ID_915', 'Std_ID_916', 'Std_ID_917', 'Std_ID_923', 'Std_ID_927', 'Std_ID_932', 'Std_ID_933', 'Std_ID_934', 'Std_ID_935', 'Std_ID_938', 'Std_ID_942', 'Std_ID_945', 'Std_ID_946', 'Std_ID_953', 'Std_ID_955', 'Std_ID_956', 'Std_ID_957', 'Std_ID_958', 'Std_ID_959', 'Std_ID_960', 'Std_ID_961', 'Std_ID_963', 'Std_ID_964', 'Std_ID_968', 'Std_ID_970', 'Std_ID_971', 'Std_ID_973', 'Std_ID_977', 'Std_ID_980', 'Std_ID_982', 'Std_ID_984', 'Std_ID_985', 'Std_ID_986', 'Std_ID_995', 'Std_ID_996', 'Std_ID_997'] + +BIO104 +['Std_ID_801', 'Std_ID_803', 'Std_ID_804', 'Std_ID_805', 'Std_ID_806', 'Std_ID_808', 'Std_ID_809', 'Std_ID_810', 'Std_ID_815', 'Std_ID_817', 'Std_ID_820', 'Std_ID_821', 'Std_ID_824', 'Std_ID_825', 'Std_ID_826', 'Std_ID_828', 'Std_ID_831', 'Std_ID_837', 'Std_ID_841', 'Std_ID_844', 'Std_ID_846', 'Std_ID_847', 'Std_ID_853', 'Std_ID_854', 'Std_ID_855', 'Std_ID_856', 'Std_ID_858', 'Std_ID_860', 'Std_ID_861', 'Std_ID_862', 'Std_ID_863', 'Std_ID_866', 'Std_ID_870', 'Std_ID_872', 'Std_ID_876', 'Std_ID_879', 'Std_ID_881', 'Std_ID_883', 'Std_ID_886', 'Std_ID_888', 'Std_ID_890', 'Std_ID_891', 'Std_ID_893', 'Std_ID_894', 'Std_ID_895', 'Std_ID_899', 'Std_ID_901', 'Std_ID_902', 'Std_ID_905', 'Std_ID_913', 'Std_ID_915', 'Std_ID_917', 'Std_ID_918', 'Std_ID_919', 'Std_ID_920', 'Std_ID_922', 'Std_ID_925', 'Std_ID_926', 'Std_ID_927', 'Std_ID_928', 'Std_ID_929', 'Std_ID_930', 'Std_ID_932', 'Std_ID_933', 'Std_ID_935', 'Std_ID_938', 'Std_ID_939', 'Std_ID_941', 'Std_ID_950', 'Std_ID_951', 'Std_ID_952', 'Std_ID_953', 'Std_ID_955', 'Std_ID_956', 'Std_ID_958', 'Std_ID_959', 'Std_ID_962', 'Std_ID_965', 'Std_ID_968', 'Std_ID_969', 'Std_ID_970', 'Std_ID_971', 'Std_ID_973', 'Std_ID_974', 'Std_ID_978', 'Std_ID_979', 'Std_ID_980', 'Std_ID_981', 'Std_ID_982', 'Std_ID_984', 'Std_ID_986', 'Std_ID_988', 'Std_ID_991', 'Std_ID_995', 'Std_ID_996', 'Std_ID_997'] + +BIO105 +['Std_ID_803', 'Std_ID_814', 'Std_ID_815', 'Std_ID_820', 'Std_ID_821', 'Std_ID_824', 'Std_ID_832', 'Std_ID_833', 'Std_ID_835', 'Std_ID_836', 'Std_ID_837', 'Std_ID_844', 'Std_ID_847', 'Std_ID_850', 'Std_ID_853', 'Std_ID_857', 'Std_ID_862', 'Std_ID_863', 'Std_ID_865', 'Std_ID_875', 'Std_ID_878', 'Std_ID_882', 'Std_ID_885', 'Std_ID_888', 'Std_ID_892', 'Std_ID_893', 'Std_ID_895', 'Std_ID_896', 'Std_ID_898', 'Std_ID_900', 'Std_ID_901', 'Std_ID_902', 'Std_ID_904', 'Std_ID_911', 'Std_ID_919', 'Std_ID_921', 'Std_ID_922', 'Std_ID_924', 'Std_ID_925', 'Std_ID_927', 'Std_ID_928', 'Std_ID_929', 'Std_ID_930', 'Std_ID_931', 'Std_ID_932', 'Std_ID_933', 'Std_ID_937', 'Std_ID_940', 'Std_ID_946', 'Std_ID_947', 'Std_ID_950', 'Std_ID_954', 'Std_ID_956', 'Std_ID_957', 'Std_ID_964', 'Std_ID_975', 'Std_ID_981', 'Std_ID_986', 'Std_ID_987', 'Std_ID_988', 'Std_ID_991', 'Std_ID_992'] + +BIO106 +['Std_ID_813', 'Std_ID_815', 'Std_ID_816', 'Std_ID_817', 'Std_ID_819', 'Std_ID_824', 'Std_ID_826', 'Std_ID_827', 'Std_ID_829', 'Std_ID_830', 'Std_ID_831', 'Std_ID_836', 'Std_ID_838', 'Std_ID_840', 'Std_ID_842', 'Std_ID_851', 'Std_ID_852', 'Std_ID_856', 'Std_ID_858', 'Std_ID_863', 'Std_ID_867', 'Std_ID_869', 'Std_ID_873', 'Std_ID_874', 'Std_ID_875', 'Std_ID_880', 'Std_ID_881', 'Std_ID_882', 'Std_ID_884', 'Std_ID_888', 'Std_ID_889', 'Std_ID_891', 'Std_ID_894', 'Std_ID_895', 'Std_ID_900', 'Std_ID_901', 'Std_ID_903', 'Std_ID_906', 'Std_ID_910', 'Std_ID_911', 'Std_ID_912', 'Std_ID_913', 'Std_ID_914', 'Std_ID_917', 'Std_ID_920', 'Std_ID_922', 'Std_ID_924', 'Std_ID_926', 'Std_ID_927', 'Std_ID_928', 'Std_ID_930', 'Std_ID_936', 'Std_ID_937', 'Std_ID_940', 'Std_ID_943', 'Std_ID_945', 'Std_ID_946', 'Std_ID_947', 'Std_ID_948', 'Std_ID_949', 'Std_ID_952', 'Std_ID_953', 'Std_ID_955', 'Std_ID_957', 'Std_ID_958', 'Std_ID_962', 'Std_ID_965', 'Std_ID_966', 'Std_ID_968', 'Std_ID_970', 'Std_ID_972', 'Std_ID_973', 'Std_ID_977', 'Std_ID_979', 'Std_ID_981', 'Std_ID_985', 'Std_ID_987', 'Std_ID_988', 'Std_ID_989', 'Std_ID_990', 'Std_ID_992', 'Std_ID_993', 'Std_ID_994', 'Std_ID_995', 'Std_ID_998', 'Std_ID_999'] + +BIO107 +['Std_ID_802', 'Std_ID_805', 'Std_ID_806', 'Std_ID_808', 'Std_ID_811', 'Std_ID_813', 'Std_ID_814', 'Std_ID_815', 'Std_ID_816', 'Std_ID_818', 'Std_ID_821', 'Std_ID_823', 'Std_ID_825', 'Std_ID_827', 'Std_ID_829', 'Std_ID_831', 'Std_ID_833', 'Std_ID_834', 'Std_ID_837', 'Std_ID_838', 'Std_ID_845', 'Std_ID_846', 'Std_ID_850', 'Std_ID_853', 'Std_ID_854', 'Std_ID_855', 'Std_ID_859', 'Std_ID_860', 'Std_ID_861', 'Std_ID_871', 'Std_ID_874', 'Std_ID_876', 'Std_ID_877', 'Std_ID_878', 'Std_ID_879', 'Std_ID_880', 'Std_ID_882', 'Std_ID_884', 'Std_ID_885', 'Std_ID_895', 'Std_ID_897', 'Std_ID_902', 'Std_ID_904', 'Std_ID_906', 'Std_ID_909', 'Std_ID_910', 'Std_ID_911', 'Std_ID_914', 'Std_ID_915', 'Std_ID_916', 'Std_ID_919', 'Std_ID_923', 'Std_ID_929', 'Std_ID_931', 'Std_ID_933', 'Std_ID_943', 'Std_ID_947', 'Std_ID_948', 'Std_ID_949', 'Std_ID_966', 'Std_ID_967', 'Std_ID_969', 'Std_ID_975', 'Std_ID_978', 'Std_ID_985', 'Std_ID_988', 'Std_ID_989', 'Std_ID_999', 'Std_ID_1000'] + +BIO108 +['Std_ID_804', 'Std_ID_805', 'Std_ID_808', 'Std_ID_811', 'Std_ID_813', 'Std_ID_814', 'Std_ID_816', 'Std_ID_822', 'Std_ID_823', 'Std_ID_826', 'Std_ID_828', 'Std_ID_829', 'Std_ID_834', 'Std_ID_835', 'Std_ID_839', 'Std_ID_843', 'Std_ID_845', 'Std_ID_846', 'Std_ID_851', 'Std_ID_852', 'Std_ID_854', 'Std_ID_855', 'Std_ID_861', 'Std_ID_864', 'Std_ID_865', 'Std_ID_866', 'Std_ID_867', 'Std_ID_868', 'Std_ID_871', 'Std_ID_874', 'Std_ID_879', 'Std_ID_881', 'Std_ID_883', 'Std_ID_885', 'Std_ID_886', 'Std_ID_887', 'Std_ID_889', 'Std_ID_898', 'Std_ID_899', 'Std_ID_901', 'Std_ID_905', 'Std_ID_907', 'Std_ID_918', 'Std_ID_919', 'Std_ID_921', 'Std_ID_924', 'Std_ID_925', 'Std_ID_926', 'Std_ID_928', 'Std_ID_929', 'Std_ID_934', 'Std_ID_935', 'Std_ID_939', 'Std_ID_940', 'Std_ID_942', 'Std_ID_943', 'Std_ID_945', 'Std_ID_950', 'Std_ID_952', 'Std_ID_953', 'Std_ID_955', 'Std_ID_958', 'Std_ID_960', 'Std_ID_966', 'Std_ID_967', 'Std_ID_969', 'Std_ID_972', 'Std_ID_974', 'Std_ID_976', 'Std_ID_977', 'Std_ID_978', 'Std_ID_980', 'Std_ID_983', 'Std_ID_984', 'Std_ID_986', 'Std_ID_987', 'Std_ID_989', 'Std_ID_990', 'Std_ID_991', 'Std_ID_993', 'Std_ID_994', 'Std_ID_998', 'Std_ID_999', 'Std_ID_1000'] + +BIO109 +['Std_ID_803', 'Std_ID_807', 'Std_ID_812', 'Std_ID_818', 'Std_ID_819', 'Std_ID_820', 'Std_ID_822', 'Std_ID_823', 'Std_ID_825', 'Std_ID_827', 'Std_ID_828', 'Std_ID_831', 'Std_ID_834', 'Std_ID_836', 'Std_ID_839', 'Std_ID_843', 'Std_ID_847', 'Std_ID_849', 'Std_ID_852', 'Std_ID_857', 'Std_ID_859', 'Std_ID_864', 'Std_ID_869', 'Std_ID_875', 'Std_ID_877', 'Std_ID_894', 'Std_ID_899', 'Std_ID_903', 'Std_ID_904', 'Std_ID_906', 'Std_ID_907', 'Std_ID_909', 'Std_ID_910', 'Std_ID_912', 'Std_ID_916', 'Std_ID_918', 'Std_ID_920', 'Std_ID_931', 'Std_ID_937', 'Std_ID_939', 'Std_ID_941', 'Std_ID_942', 'Std_ID_944', 'Std_ID_950', 'Std_ID_952', 'Std_ID_954', 'Std_ID_961', 'Std_ID_963', 'Std_ID_968', 'Std_ID_972', 'Std_ID_973', 'Std_ID_974', 'Std_ID_979', 'Std_ID_981', 'Std_ID_983', 'Std_ID_989', 'Std_ID_992', 'Std_ID_995'] + +BIO110 +['Std_ID_801', 'Std_ID_802', 'Std_ID_804', 'Std_ID_806', 'Std_ID_809', 'Std_ID_810', 'Std_ID_813', 'Std_ID_817', 'Std_ID_823', 'Std_ID_824', 'Std_ID_826', 'Std_ID_829', 'Std_ID_830', 'Std_ID_832', 'Std_ID_833', 'Std_ID_834', 'Std_ID_838', 'Std_ID_840', 'Std_ID_843', 'Std_ID_844', 'Std_ID_847', 'Std_ID_848', 'Std_ID_849', 'Std_ID_851', 'Std_ID_853', 'Std_ID_856', 'Std_ID_857', 'Std_ID_859', 'Std_ID_860', 'Std_ID_861', 'Std_ID_864', 'Std_ID_868', 'Std_ID_870', 'Std_ID_872', 'Std_ID_875', 'Std_ID_876', 'Std_ID_877', 'Std_ID_879', 'Std_ID_887', 'Std_ID_892', 'Std_ID_893', 'Std_ID_894', 'Std_ID_896', 'Std_ID_897', 'Std_ID_902', 'Std_ID_905', 'Std_ID_908', 'Std_ID_909', 'Std_ID_910', 'Std_ID_915', 'Std_ID_916', 'Std_ID_917', 'Std_ID_930', 'Std_ID_936', 'Std_ID_937', 'Std_ID_943', 'Std_ID_944', 'Std_ID_947', 'Std_ID_951', 'Std_ID_956', 'Std_ID_961', 'Std_ID_962', 'Std_ID_964', 'Std_ID_965', 'Std_ID_966', 'Std_ID_967', 'Std_ID_971', 'Std_ID_972', 'Std_ID_976', 'Std_ID_980', 'Std_ID_983', 'Std_ID_984', 'Std_ID_994', 'Std_ID_996', 'Std_ID_997', 'Std_ID_1000'] \ No newline at end of file diff --git a/sample_data/hard_set_5/.placeholder b/sample_data/hard_set_5/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/hard_set_5/attendance.csv b/sample_data/hard_set_5/attendance.csv new file mode 100644 index 0000000..c33dfd6 --- /dev/null +++ b/sample_data/hard_set_5/attendance.csv @@ -0,0 +1,45 @@ +ART202 +['S351', 'S352', 'S353', 'S354', 'S355', 'S356', 'S357', 'S358', 'S359', 'S360', 'S361', 'S362', 'S363', 'S364', 'S365', 'S366', 'S367', 'S368', 'S369', 'S370', 'S371', 'S372', 'S373', 'S374', 'S375', 'S376', 'S377', 'S378', 'S379', 'S380', 'S381', 'S382', 'S383', 'S384', 'S385', 'S386', 'S387', 'S388', 'S389', 'S390', 'S391', 'S392', 'S393', 'S394', 'S395', 'S396', 'S397', 'S398', 'S399', 'S400', 'S401', 'S402', 'S403', 'S404', 'S405', 'S406', 'S407', 'S408', 'S409', 'S410', 'S411', 'S412', 'S413', 'S414', 'S415', 'S416', 'S417', 'S418', 'S419', 'S420', 'S421', 'S422', 'S423', 'S424', 'S425', 'S426', 'S427', 'S428', 'S429', 'S430'] + +BIO202 +['S201', 'S202', 'S203', 'S204', 'S205', 'S206', 'S207', 'S208', 'S209', 'S210', 'S211', 'S212', 'S213', 'S214', 'S215', 'S216', 'S217', 'S218', 'S219', 'S220', 'S221', 'S222', 'S223', 'S224', 'S225', 'S226', 'S227', 'S228', 'S229', 'S230', 'S231', 'S232', 'S233', 'S234', 'S235', 'S236', 'S237', 'S238', 'S239', 'S240', 'S241', 'S242', 'S243', 'S244', 'S245', 'S246', 'S247', 'S248', 'S249', 'S250', 'S251', 'S252', 'S253', 'S254', 'S255', 'S256', 'S257', 'S258', 'S259', 'S260', 'S261', 'S262', 'S263', 'S264', 'S265', 'S266', 'S267', 'S268', 'S269', 'S270', 'S271', 'S272', 'S273', 'S274', 'S275', 'S276', 'S277', 'S278', 'S279', 'S280', 'S281', 'S282', 'S283', 'S284', 'S285', 'S286', 'S287', 'S288', 'S289', 'S290', 'S291', 'S292', 'S293', 'S294', 'S295', 'S296', 'S297', 'S298', 'S299', 'S300'] + +CHE301 +['S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110', 'S111', 'S112', 'S113', 'S114', 'S115', 'S116', 'S117', 'S118', 'S119', 'S120', 'S121', 'S122', 'S123', 'S124', 'S125', 'S126', 'S127', 'S128', 'S129', 'S130', 'S131', 'S132', 'S133', 'S134', 'S135', 'S136', 'S137', 'S138', 'S139', 'S140', 'S141', 'S142', 'S143', 'S144', 'S145', 'S146', 'S147', 'S148', 'S149', 'S150', 'S151', 'S152', 'S153', 'S154', 'S155', 'S156', 'S157', 'S158', 'S159', 'S160', 'S161', 'S162', 'S163', 'S164', 'S165', 'S166', 'S167', 'S168', 'S169', 'S170', 'S171', 'S172', 'S173', 'S174', 'S175', 'S176', 'S177', 'S178', 'S179', 'S180', 'S181', 'S182', 'S183', 'S184', 'S185', 'S186', 'S187', 'S188', 'S189', 'S190', 'S191', 'S192', 'S193', 'S194', 'S195', 'S196', 'S197', 'S198', 'S199', 'S200'] + +CSE101 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050', 'S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100', 'S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110', 'S111', 'S112', 'S113', 'S114', 'S115', 'S116', 'S117', 'S118', 'S119', 'S120', 'S121', 'S122', 'S123', 'S124', 'S125', 'S126', 'S127', 'S128', 'S129', 'S130', 'S131', 'S132', 'S133', 'S134', 'S135', 'S136', 'S137', 'S138', 'S139', 'S140', 'S141', 'S142', 'S143', 'S144', 'S145', 'S146', 'S147', 'S148', 'S149', 'S150', 'S151', 'S152', 'S153', 'S154', 'S155', 'S156', 'S157', 'S158', 'S159', 'S160', 'S161', 'S162', 'S163', 'S164', 'S165', 'S166', 'S167', 'S168', 'S169', 'S170', 'S171', 'S172', 'S173', 'S174', 'S175', 'S176', 'S177', 'S178', 'S179', 'S180', 'S181', 'S182', 'S183', 'S184', 'S185', 'S186', 'S187', 'S188', 'S189', 'S190', 'S191', 'S192', 'S193', 'S194', 'S195', 'S196', 'S197', 'S198', 'S199', 'S200'] + +ECO210 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050', 'S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100', 'S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110', 'S111', 'S112', 'S113', 'S114', 'S115', 'S116', 'S117', 'S118', 'S119', 'S120'] + +EE401 +['S201', 'S202', 'S203', 'S204', 'S205', 'S206', 'S207', 'S208', 'S209', 'S210', 'S211', 'S212', 'S213', 'S214', 'S215', 'S216', 'S217', 'S218', 'S219', 'S220', 'S221', 'S222', 'S223', 'S224', 'S225', 'S226', 'S227', 'S228', 'S229', 'S230', 'S231', 'S232', 'S233', 'S234', 'S235', 'S236', 'S237', 'S238', 'S239', 'S240', 'S241', 'S242', 'S243', 'S244', 'S245', 'S246', 'S247', 'S248', 'S249', 'S250', 'S251', 'S252', 'S253', 'S254', 'S255', 'S256', 'S257', 'S258', 'S259', 'S260', 'S261', 'S262', 'S263', 'S264', 'S265', 'S266', 'S267', 'S268', 'S269', 'S270'] + +ENG101 +['S201', 'S202', 'S203', 'S204', 'S205', 'S206', 'S207', 'S208', 'S209', 'S210', 'S211', 'S212', 'S213', 'S214', 'S215', 'S216', 'S217', 'S218', 'S219', 'S220', 'S221', 'S222', 'S223', 'S224', 'S225', 'S226', 'S227', 'S228', 'S229', 'S230', 'S231', 'S232', 'S233', 'S234', 'S235', 'S236', 'S237', 'S238', 'S239', 'S240', 'S241', 'S242', 'S243', 'S244', 'S245', 'S246', 'S247', 'S248', 'S249', 'S250', 'S251', 'S252', 'S253', 'S254', 'S255', 'S256', 'S257', 'S258', 'S259', 'S260', 'S261', 'S262', 'S263', 'S264', 'S265', 'S266', 'S267', 'S268', 'S269', 'S270', 'S271', 'S272', 'S273', 'S274', 'S275', 'S276', 'S277', 'S278', 'S279', 'S280', 'S281', 'S282', 'S283', 'S284', 'S285', 'S286', 'S287', 'S288', 'S289', 'S290', 'S291', 'S292', 'S293', 'S294', 'S295', 'S296', 'S297', 'S298', 'S299', 'S300', 'S301', 'S302', 'S303', 'S304', 'S305', 'S306', 'S307', 'S308', 'S309', 'S310', 'S311', 'S312', 'S313', 'S314', 'S315', 'S316', 'S317', 'S318', 'S319', 'S320', 'S321', 'S322', 'S323', 'S324', 'S325', 'S326', 'S327', 'S328', 'S329', 'S330', 'S331', 'S332', 'S333', 'S334', 'S335', 'S336', 'S337', 'S338', 'S339', 'S340', 'S341', 'S342', 'S343', 'S344', 'S345', 'S346', 'S347', 'S348', 'S349', 'S350'] + +GEO101 +['S401', 'S402', 'S403', 'S404', 'S405', 'S406', 'S407', 'S408', 'S409', 'S410', 'S411', 'S412', 'S413', 'S414', 'S415', 'S416', 'S417', 'S418', 'S419', 'S420', 'S421', 'S422', 'S423', 'S424', 'S425', 'S426', 'S427', 'S428', 'S429', 'S430', 'S431', 'S432', 'S433', 'S434', 'S435', 'S436', 'S437', 'S438', 'S439', 'S440', 'S441', 'S442', 'S443', 'S444', 'S445', 'S446', 'S447', 'S448', 'S449', 'S450', 'S451', 'S452', 'S453', 'S454', 'S455', 'S456', 'S457', 'S458', 'S459', 'S460', 'S461', 'S462', 'S463', 'S464', 'S465', 'S466', 'S467', 'S468', 'S469', 'S470', 'S471', 'S472', 'S473', 'S474', 'S475', 'S476', 'S477', 'S478', 'S479', 'S480', 'S481', 'S482', 'S483', 'S484', 'S485', 'S486', 'S487', 'S488', 'S489', 'S490', 'S491', 'S492', 'S493', 'S494', 'S495', 'S496', 'S497', 'S498', 'S499', 'S500'] + +HIS105 +['S301', 'S302', 'S303', 'S304', 'S305', 'S306', 'S307', 'S308', 'S309', 'S310', 'S311', 'S312', 'S313', 'S314', 'S315', 'S316', 'S317', 'S318', 'S319', 'S320', 'S321', 'S322', 'S323', 'S324', 'S325', 'S326', 'S327', 'S328', 'S329', 'S330', 'S331', 'S332', 'S333', 'S334', 'S335', 'S336', 'S337', 'S338', 'S339', 'S340', 'S341', 'S342', 'S343', 'S344', 'S345', 'S346', 'S347', 'S348', 'S349', 'S350', 'S351', 'S352', 'S353', 'S354', 'S355', 'S356', 'S357', 'S358', 'S359', 'S360', 'S361', 'S362', 'S363', 'S364', 'S365', 'S366', 'S367', 'S368', 'S369', 'S370', 'S371', 'S372', 'S373', 'S374', 'S375', 'S376', 'S377', 'S378', 'S379', 'S380'] + +MAT202 +['S201', 'S202', 'S203', 'S204', 'S205', 'S206', 'S207', 'S208', 'S209', 'S210', 'S211', 'S212', 'S213', 'S214', 'S215', 'S216', 'S217', 'S218', 'S219', 'S220', 'S221', 'S222', 'S223', 'S224', 'S225', 'S226', 'S227', 'S228', 'S229', 'S230', 'S231', 'S232', 'S233', 'S234', 'S235', 'S236', 'S237', 'S238', 'S239', 'S240', 'S241', 'S242', 'S243', 'S244', 'S245', 'S246', 'S247', 'S248', 'S249', 'S250', 'S251', 'S252', 'S253', 'S254', 'S255', 'S256', 'S257', 'S258', 'S259', 'S260', 'S261', 'S262', 'S263', 'S264', 'S265', 'S266', 'S267', 'S268', 'S269', 'S270', 'S271', 'S272', 'S273', 'S274', 'S275', 'S276', 'S277', 'S278', 'S279', 'S280', 'S281', 'S282', 'S283', 'S284', 'S285', 'S286', 'S287', 'S288', 'S289', 'S290', 'S291', 'S292', 'S293', 'S294', 'S295'] + +MUS303 +['S401', 'S402', 'S403', 'S404', 'S405', 'S406', 'S407', 'S408', 'S409', 'S410', 'S411', 'S412', 'S413', 'S414', 'S415', 'S416', 'S417', 'S418', 'S419', 'S420', 'S421', 'S422', 'S423', 'S424', 'S425', 'S426', 'S427', 'S428', 'S429', 'S430', 'S431', 'S432', 'S433', 'S434', 'S435', 'S436', 'S437', 'S438', 'S439', 'S440', 'S441', 'S442', 'S443', 'S444', 'S445', 'S446', 'S447', 'S448', 'S449', 'S450', 'S451', 'S452', 'S453', 'S454', 'S455', 'S456', 'S457', 'S458', 'S459', 'S460', 'S461', 'S462', 'S463', 'S464', 'S465', 'S466', 'S467', 'S468', 'S469', 'S470', 'S471', 'S472', 'S473', 'S474', 'S475', 'S476', 'S477', 'S478', 'S479', 'S480'] + +PHI401 +['S301', 'S302', 'S303', 'S304', 'S305', 'S306', 'S307', 'S308', 'S309', 'S310', 'S311', 'S312', 'S313', 'S314', 'S315', 'S316', 'S317', 'S318', 'S319', 'S320', 'S321', 'S322', 'S323', 'S324', 'S325', 'S326', 'S327', 'S328', 'S329', 'S330', 'S331', 'S332', 'S333', 'S334', 'S335', 'S336', 'S337', 'S338', 'S339', 'S340', 'S341', 'S342', 'S343', 'S344', 'S345', 'S346', 'S347', 'S348', 'S349', 'S350', 'S351', 'S352', 'S353', 'S354', 'S355', 'S356', 'S357', 'S358', 'S359', 'S360', 'S361', 'S362', 'S363', 'S364', 'S365', 'S366', 'S367', 'S368', 'S369', 'S370', 'S371', 'S372', 'S373', 'S374', 'S375', 'S376', 'S377', 'S378', 'S379', 'S380', 'S381', 'S382', 'S383', 'S384', 'S385', 'S386', 'S387', 'S388', 'S389', 'S390', 'S391', 'S392', 'S393', 'S394', 'S395', 'S396', 'S397', 'S398', 'S399', 'S400'] + +PHY301 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050', 'S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100', 'S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110', 'S111', 'S112', 'S113', 'S114', 'S115', 'S116', 'S117', 'S118', 'S119', 'S120', 'S121', 'S122', 'S123', 'S124', 'S125', 'S126', 'S127', 'S128', 'S129', 'S130', 'S131', 'S132', 'S133', 'S134', 'S135', 'S136', 'S137', 'S138', 'S139', 'S140', 'S141', 'S142', 'S143', 'S144', 'S145', 'S146', 'S147', 'S148', 'S149', 'S150'] + +PSY401 +['S401', 'S402', 'S403', 'S404', 'S405', 'S406', 'S407', 'S408', 'S409', 'S410', 'S411', 'S412', 'S413', 'S414', 'S415', 'S416', 'S417', 'S418', 'S419', 'S420', 'S421', 'S422', 'S423', 'S424', 'S425', 'S426', 'S427', 'S428', 'S429', 'S430', 'S431', 'S432', 'S433', 'S434', 'S435', 'S436', 'S437', 'S438', 'S439', 'S440', 'S441', 'S442', 'S443', 'S444', 'S445', 'S446', 'S447', 'S448', 'S449', 'S450', 'S451', 'S452', 'S453', 'S454', 'S455', 'S456', 'S457', 'S458', 'S459', 'S460', 'S461', 'S462', 'S463', 'S464', 'S465', 'S466', 'S467', 'S468', 'S469', 'S470', 'S471', 'S472', 'S473', 'S474', 'S475', 'S476', 'S477', 'S478', 'S479', 'S480', 'S481', 'S482', 'S483', 'S484', 'S485', 'S486', 'S487', 'S488', 'S489', 'S490', 'S491', 'S492', 'S493', 'S494', 'S495', 'S496', 'S497', 'S498', 'S499', 'S500'] + +SOC101 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050', 'S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100'] + diff --git a/sample_data/hard_set_5/classrooms.csv b/sample_data/hard_set_5/classrooms.csv new file mode 100644 index 0000000..fc09e85 --- /dev/null +++ b/sample_data/hard_set_5/classrooms.csv @@ -0,0 +1,13 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +R1;190 +R10;180 +R2;170 +R3;160 +R4;150 +R5;180 +R6;190 +R7;170 +R8;180 +R9;155 +R10;175 +R11;164 diff --git a/sample_data/hard_set_5/courses.csv b/sample_data/hard_set_5/courses.csv new file mode 100644 index 0000000..975a6b9 --- /dev/null +++ b/sample_data/hard_set_5/courses.csv @@ -0,0 +1,16 @@ +ALL OF THE COURSES IN THE SYSTEM +ART202 +BIO202 +CHE301 +CSE101 +ECO210 +EE401 +ENG101 +GEO101 +HIS105 +MAT202 +MUS303 +PHI401 +PHY301 +PSY401 +SOC101 diff --git a/sample_data/hard_set_5/students.csv b/sample_data/hard_set_5/students.csv new file mode 100644 index 0000000..407d898 --- /dev/null +++ b/sample_data/hard_set_5/students.csv @@ -0,0 +1,501 @@ +ALL OF THE STUDENTS IN THE SYSTEM +S001 +S002 +S003 +S004 +S005 +S006 +S007 +S008 +S009 +S010 +S011 +S012 +S013 +S014 +S015 +S016 +S017 +S018 +S019 +S020 +S021 +S022 +S023 +S024 +S025 +S026 +S027 +S028 +S029 +S030 +S031 +S032 +S033 +S034 +S035 +S036 +S037 +S038 +S039 +S040 +S041 +S042 +S043 +S044 +S045 +S046 +S047 +S048 +S049 +S050 +S051 +S052 +S053 +S054 +S055 +S056 +S057 +S058 +S059 +S060 +S061 +S062 +S063 +S064 +S065 +S066 +S067 +S068 +S069 +S070 +S071 +S072 +S073 +S074 +S075 +S076 +S077 +S078 +S079 +S080 +S081 +S082 +S083 +S084 +S085 +S086 +S087 +S088 +S089 +S090 +S091 +S092 +S093 +S094 +S095 +S096 +S097 +S098 +S099 +S100 +S101 +S102 +S103 +S104 +S105 +S106 +S107 +S108 +S109 +S110 +S111 +S112 +S113 +S114 +S115 +S116 +S117 +S118 +S119 +S120 +S121 +S122 +S123 +S124 +S125 +S126 +S127 +S128 +S129 +S130 +S131 +S132 +S133 +S134 +S135 +S136 +S137 +S138 +S139 +S140 +S141 +S142 +S143 +S144 +S145 +S146 +S147 +S148 +S149 +S150 +S151 +S152 +S153 +S154 +S155 +S156 +S157 +S158 +S159 +S160 +S161 +S162 +S163 +S164 +S165 +S166 +S167 +S168 +S169 +S170 +S171 +S172 +S173 +S174 +S175 +S176 +S177 +S178 +S179 +S180 +S181 +S182 +S183 +S184 +S185 +S186 +S187 +S188 +S189 +S190 +S191 +S192 +S193 +S194 +S195 +S196 +S197 +S198 +S199 +S200 +S201 +S202 +S203 +S204 +S205 +S206 +S207 +S208 +S209 +S210 +S211 +S212 +S213 +S214 +S215 +S216 +S217 +S218 +S219 +S220 +S221 +S222 +S223 +S224 +S225 +S226 +S227 +S228 +S229 +S230 +S231 +S232 +S233 +S234 +S235 +S236 +S237 +S238 +S239 +S240 +S241 +S242 +S243 +S244 +S245 +S246 +S247 +S248 +S249 +S250 +S251 +S252 +S253 +S254 +S255 +S256 +S257 +S258 +S259 +S260 +S261 +S262 +S263 +S264 +S265 +S266 +S267 +S268 +S269 +S270 +S271 +S272 +S273 +S274 +S275 +S276 +S277 +S278 +S279 +S280 +S281 +S282 +S283 +S284 +S285 +S286 +S287 +S288 +S289 +S290 +S291 +S292 +S293 +S294 +S295 +S296 +S297 +S298 +S299 +S300 +S301 +S302 +S303 +S304 +S305 +S306 +S307 +S308 +S309 +S310 +S311 +S312 +S313 +S314 +S315 +S316 +S317 +S318 +S319 +S320 +S321 +S322 +S323 +S324 +S325 +S326 +S327 +S328 +S329 +S330 +S331 +S332 +S333 +S334 +S335 +S336 +S337 +S338 +S339 +S340 +S341 +S342 +S343 +S344 +S345 +S346 +S347 +S348 +S349 +S350 +S351 +S352 +S353 +S354 +S355 +S356 +S357 +S358 +S359 +S360 +S361 +S362 +S363 +S364 +S365 +S366 +S367 +S368 +S369 +S370 +S371 +S372 +S373 +S374 +S375 +S376 +S377 +S378 +S379 +S380 +S381 +S382 +S383 +S384 +S385 +S386 +S387 +S388 +S389 +S390 +S391 +S392 +S393 +S394 +S395 +S396 +S397 +S398 +S399 +S400 +S401 +S402 +S403 +S404 +S405 +S406 +S407 +S408 +S409 +S410 +S411 +S412 +S413 +S414 +S415 +S416 +S417 +S418 +S419 +S420 +S421 +S422 +S423 +S424 +S425 +S426 +S427 +S428 +S429 +S430 +S431 +S432 +S433 +S434 +S435 +S436 +S437 +S438 +S439 +S440 +S441 +S442 +S443 +S444 +S445 +S446 +S447 +S448 +S449 +S450 +S451 +S452 +S453 +S454 +S455 +S456 +S457 +S458 +S459 +S460 +S461 +S462 +S463 +S464 +S465 +S466 +S467 +S468 +S469 +S470 +S471 +S472 +S473 +S474 +S475 +S476 +S477 +S478 +S479 +S480 +S481 +S482 +S483 +S484 +S485 +S486 +S487 +S488 +S489 +S490 +S491 +S492 +S493 +S494 +S495 +S496 +S497 +S498 +S499 +S500 diff --git a/sample_data/impossible_set_4/.placeholder b/sample_data/impossible_set_4/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/impossible_set_4/set4_attendance.csv b/sample_data/impossible_set_4/set4_attendance.csv new file mode 100644 index 0000000..1b4cc80 --- /dev/null +++ b/sample_data/impossible_set_4/set4_attendance.csv @@ -0,0 +1,8 @@ +BIO620 +['S4_001', 'S4_002', 'S4_003', 'S4_004', 'S4_005', 'S4_006', 'S4_007', 'S4_008', 'S4_009', 'S4_010', 'S4_011', 'S4_012', 'S4_013', 'S4_014', 'S4_015', 'S4_016', 'S4_017', 'S4_018', 'S4_019', 'S4_020', 'S4_021', 'S4_022', 'S4_023', 'S4_024', 'S4_025', 'S4_026', 'S4_027', 'S4_028', 'S4_029', 'S4_030', 'S4_031', 'S4_032', 'S4_033', 'S4_034', 'S4_035', 'S4_036', 'S4_037', 'S3_038', 'S4_039', 'S4_040', 'S4_041', 'S4_042', 'S4_043', 'S4_044', 'S4_045', 'S4_046', 'S4_047', 'S4_048', 'S4_049', 'S4_050'] + +CHEM630 +['S4_051', 'S4_052', 'S4_053', 'S4_054', 'S4_055', 'S4_056', 'S4_057', 'S4_058', 'S4_059', 'S4_060', 'S4_061', 'S4_062', 'S4_063', 'S4_064', 'S4_065'] + +NEURO701 +['S4_066', 'S4_067', 'S4_068', 'S4_069', 'S4_070', 'S4_071', 'S4_072', 'S4_073', 'S4_074', 'S4_075', 'S4_076', 'S4_077', 'S4_078', 'S4_079', 'S4_080', 'S4_081', 'S4_082', 'S4_083', 'S4_084', 'S4_085'] diff --git a/sample_data/impossible_set_4/set4_classrooms.csv b/sample_data/impossible_set_4/set4_classrooms.csv new file mode 100644 index 0000000..99175d7 --- /dev/null +++ b/sample_data/impossible_set_4/set4_classrooms.csv @@ -0,0 +1,3 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +LAB-A;15 +LAB-B;20 diff --git a/sample_data/impossible_set_4/set4_courses.csv b/sample_data/impossible_set_4/set4_courses.csv new file mode 100644 index 0000000..b05faa2 --- /dev/null +++ b/sample_data/impossible_set_4/set4_courses.csv @@ -0,0 +1,4 @@ +ALL OF THE COURSES IN THE SYSTEM +BIO620 +CHEM630 +NEURO701 diff --git a/sample_data/impossible_set_4/set4_students.csv b/sample_data/impossible_set_4/set4_students.csv new file mode 100644 index 0000000..d876cdb --- /dev/null +++ b/sample_data/impossible_set_4/set4_students.csv @@ -0,0 +1,101 @@ +ALL OF THE STUDENTS IN THE SYSTEM +S4_001 +S4_002 +S4_003 +S4_004 +S4_005 +S4_006 +S4_007 +S4_008 +S4_009 +S4_010 +S4_011 +S4_012 +S4_013 +S4_014 +S4_015 +S4_016 +S4_017 +S4_018 +S4_019 +S4_020 +S4_021 +S4_022 +S4_023 +S4_024 +S4_025 +S4_026 +S4_027 +S4_028 +S4_029 +S4_030 +S4_031 +S4_032 +S4_033 +S4_034 +S4_035 +S4_036 +S4_037 +S4_038 +S4_039 +S4_040 +S4_041 +S4_042 +S4_043 +S4_044 +S4_045 +S4_046 +S4_047 +S4_048 +S4_049 +S4_050 +S4_051 +S4_052 +S4_053 +S4_054 +S4_055 +S4_056 +S4_057 +S4_058 +S4_059 +S4_060 +S4_061 +S4_062 +S4_063 +S4_064 +S4_065 +S4_066 +S4_067 +S4_068 +S4_069 +S4_070 +S4_071 +S4_072 +S4_073 +S4_074 +S4_075 +S4_076 +S4_077 +S4_078 +S4_079 +S4_080 +S4_081 +S4_082 +S4_083 +S4_084 +S4_085 +S4_086 +S4_087 +S4_088 +S4_089 +S4_090 +S4_091 +S4_092 +S4_093 +S4_094 +S4_095 +S4_096 +S4_097 +S4_098 +S4_099 +S4_100 diff --git a/sample_data/splitexample_set_3/.placeholder b/sample_data/splitexample_set_3/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/splitexample_set_3/set3_attendance.csv b/sample_data/splitexample_set_3/set3_attendance.csv new file mode 100644 index 0000000..76999cf --- /dev/null +++ b/sample_data/splitexample_set_3/set3_attendance.csv @@ -0,0 +1,17 @@ +ARCH501 +['S3_001', 'S3_002', 'S3_003', 'S3_004', 'S3_005', 'S3_006', 'S3_007', 'S3_008', 'S3_009', 'S3_010', 'S3_011', 'S3_012', 'S3_013', 'S3_014', 'S3_015', 'S3_016', 'S3_017', 'S3_018', 'S3_019', 'S3_020', 'S3_021', 'S3_022', 'S3_023', 'S3_024', 'S3_025', 'S3_026', 'S3_027', 'S3_028', 'S3_029', 'S3_030', 'S3_031', 'S3_032', 'S3_033', 'S3_034', 'S3_035', 'S3_036', 'S3_037', 'S3_038', 'S3_039', 'S3_040', 'S3_041', 'S3_042', 'S3_043', 'S3_044', 'S3_045', 'S3_046', 'S3_047', 'S3_048', 'S3_049', 'S3_050', 'S3_051', 'S3_052', 'S3_053', 'S3_054', 'S3_055', 'S3_056', 'S3_057', 'S3_058', 'S3_059', 'S3_060', 'S3_061', 'S3_062', 'S3_063', 'S3_064', 'S3_065', 'S3_066', 'S3_067', 'S3_068', 'S3_069', 'S3_070', 'S3_071', 'S3_072', 'S3_073', 'S3_074', 'S3_075', 'S3_076', 'S3_077', 'S3_078', 'S3_079', 'S3_080', 'S3_081', 'S3_082', 'S3_083', 'S3_084', 'S3_085', 'S3_086', 'S3_087', 'S3_088', 'S3_089', 'S3_090', 'S3_091', 'S3_092', 'S3_093', 'S3_094', 'S3_095', 'S3_096', 'S3_097', 'S3_098', 'S3_099', 'S3_100', 'S3_101', 'S3_102', 'S3_103', 'S3_104', 'S3_105', 'S3_106', 'S3_107', 'S3_108', 'S3_109', 'S3_110', 'S3_111', 'S3_112', 'S3_113', 'S3_114', 'S3_115', 'S3_116', 'S3_117', 'S3_118', 'S3_119', 'S3_120'] + +ARCH512 +['S3_121', 'S3_122', 'S3_123', 'S3_124', 'S3_125', 'S3_126', 'S3_127', 'S3_128', 'S3_129', 'S3_130', 'S3_131', 'S3_132', 'S3_133', 'S3_134', 'S3_135', 'S3_136', 'S3_137', 'S3_138', 'S3_139', 'S3_140', 'S3_141', 'S3_142', 'S3_143', 'S3_144', 'S3_145', 'S3_146', 'S3_147', 'S3_148', 'S3_149', 'S3_150', 'S3_151', 'S3_152', 'S3_153', 'S3_154', 'S3_155', 'S3_156', 'S3_157', 'S3_158', 'S3_159', 'S3_160', 'S3_161', 'S3_162', 'S3_163', 'S3_164', 'S3_165'] + +URP301 +['S3_001', 'S3_002', 'S3_003', 'S3_004', 'S3_005', 'S3_011', 'S3_012', 'S3_013', 'S3_014', 'S3_015', 'S3_021', 'S3_022', 'S3_023', 'S3_024', 'S3_025', 'S3_166', 'S3_167', 'S3_168', 'S3_169', 'S3_170', 'S3_171', 'S3_172', 'S3_173', 'S3_174', 'S3_175', 'S3_176', 'S3_177', 'S3_178', 'S3_179', 'S3_180', 'S3_031', 'S3_032', 'S3_033', 'S3_034', 'S3_035', 'S3_041', 'S3_042', 'S3_043', 'S3_044', 'S3_045', 'S3_051', 'S3_052', 'S3_053', 'S3_054', 'S3_055', 'S3_061', 'S3_062', 'S3_063', 'S3_064', 'S3_065', 'S3_071', 'S3_072', 'S3_073', 'S3_074', 'S3_075', 'S3_081', 'S3_082', 'S3_083', 'S3_084', 'S3_085', 'S3_091', 'S3_092', 'S3_093', 'S3_094', 'S3_095', 'S3_101', 'S3_102', 'S3_103', 'S3_104', 'S3_105'] + +IND411 +['S3_156', 'S3_157', 'S3_158', 'S3_159', 'S3_160', 'S3_161', 'S3_162', 'S3_163', 'S3_164', 'S3_165', 'S3_166', 'S3_167', 'S3_168', 'S3_169', 'S3_170', 'S3_171', 'S3_172', 'S3_173', 'S3_174', 'S3_175', 'S3_176', 'S3_177', 'S3_178', 'S3_179', 'S3_180'] + +ART101 +['S3_001', 'S3_002', 'S3_003', 'S3_004', 'S3_005', 'S3_006', 'S3_007', 'S3_008', 'S3_009', 'S3_010', 'S3_011', 'S3_012', 'S3_013', 'S3_014', 'S3_015', 'S3_016', 'S3_017', 'S3_018', 'S3_019', 'S3_020', 'S3_021', 'S3_022', 'S3_023', 'S3_024', 'S3_025', 'S3_026', 'S3_027', 'S3_028', 'S3_029', 'S3_030', 'S3_031', 'S3_032', 'S3_033', 'S3_034', 'S3_035', 'S3_036', 'S3_037', 'S3_038', 'S3_039', 'S3_040', 'S3_041', 'S3_042', 'S3_043', 'S3_044', 'S3_045', 'S3_046', 'S3_047', 'S3_048', 'S3_049', 'S3_050', 'S3_131', 'S3_132', 'S3_133', 'S3_134', 'S3_135', 'S3_136', 'S3_137', 'S3_138', 'S3_139', 'S3_140', 'S3_141', 'S3_142', 'S3_143', 'S3_144', 'S3_145', 'S3_146', 'S3_147', 'S3_148', 'S3_149', 'S3_150', 'S3_151', 'S3_152', 'S3_153', 'S3_154', 'S3_155', 'S3_156', 'S3_157', 'S3_158', 'S3_159', 'S3_160', 'S3_161', 'S3_162', 'S3_163', 'S3_164', 'S3_165', 'S3_166', 'S3_167', 'S3_168', 'S3_169', 'S3_170', 'S3_171', 'S3_172', 'S3_173', 'S3_174', 'S3_175', 'S3_176', 'S3_177', 'S3_178', 'S3_179', 'S3_180'] + +ART102 +['S3_051', 'S3_052', 'S3_053', 'S3_054', 'S3_055', 'S3_056', 'S3_057', 'S3_058', 'S3_059', 'S3_060', 'S3_061', 'S3_062', 'S3_063', 'S3_064', 'S3_065', 'S3_066', 'S3_067', 'S3_068', 'S3_069', 'S3_070'] diff --git a/sample_data/splitexample_set_3/set3_classrooms.csv b/sample_data/splitexample_set_3/set3_classrooms.csv new file mode 100644 index 0000000..72706d1 --- /dev/null +++ b/sample_data/splitexample_set_3/set3_classrooms.csv @@ -0,0 +1,6 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +F-A01;20 +F-A02;25 +F-B01;30 +F-B02;30 +F-C01;50 diff --git a/sample_data/splitexample_set_3/set3_courses.csv b/sample_data/splitexample_set_3/set3_courses.csv new file mode 100644 index 0000000..951424d --- /dev/null +++ b/sample_data/splitexample_set_3/set3_courses.csv @@ -0,0 +1,7 @@ +ALL OF THE COURSES IN THE SYSTEM +ARCH501 +ARCH512 +URP301 +IND411 +ART101 +ART102 diff --git a/sample_data/splitexample_set_3/set3_students.csv b/sample_data/splitexample_set_3/set3_students.csv new file mode 100644 index 0000000..a462162 --- /dev/null +++ b/sample_data/splitexample_set_3/set3_students.csv @@ -0,0 +1,181 @@ +ALL OF THE STUDENTS IN THE SYSTEM +S3_001 +S3_002 +S3_003 +S3_004 +S3_005 +S3_006 +S3_007 +S3_008 +S3_009 +S3_010 +S3_011 +S3_012 +S3_013 +S3_014 +S3_015 +S3_016 +S3_017 +S3_018 +S3_019 +S3_020 +S3_021 +S3_022 +S3_023 +S3_024 +S3_025 +S3_026 +S3_027 +S3_028 +S3_029 +S3_030 +S3_031 +S3_032 +S3_033 +S3_034 +S3_035 +S3_036 +S3_037 +S3_038 +S3_039 +S3_040 +S3_041 +S3_042 +S3_043 +S3_044 +S3_045 +S3_046 +S3_047 +S3_048 +S3_049 +S3_050 +S3_051 +S3_052 +S3_053 +S3_054 +S3_055 +S3_056 +S3_057 +S3_058 +S3_059 +S3_060 +S3_061 +S3_062 +S3_063 +S3_064 +S3_065 +S3_066 +S3_067 +S3_068 +S3_069 +S3_070 +S3_071 +S3_072 +S3_073 +S3_074 +S3_075 +S3_076 +S3_077 +S3_078 +S3_079 +S3_080 +S3_081 +S3_082 +S3_083 +S3_084 +S3_085 +S3_086 +S3_087 +S3_088 +S3_089 +S3_090 +S3_091 +S3_092 +S3_093 +S3_094 +S3_095 +S3_096 +S3_097 +S3_098 +S3_099 +S3_100 +S3_101 +S3_102 +S3_103 +S3_104 +S3_105 +S3_106 +S3_107 +S3_108 +S3_109 +S3_110 +S3_111 +S3_112 +S3_113 +S3_114 +S3_115 +S3_116 +S3_117 +S3_118 +S3_119 +S3_120 +S3_121 +S3_122 +S3_123 +S3_124 +S3_125 +S3_126 +S3_127 +S3_128 +S3_129 +S3_130 +S3_131 +S3_132 +S3_133 +S3_134 +S3_135 +S3_136 +S3_137 +S3_138 +S3_139 +S3_140 +S3_141 +S3_142 +S3_143 +S3_144 +S3_145 +S3_146 +S3_147 +S3_148 +S3_149 +S3_150 +S3_151 +S3_152 +S3_153 +S3_154 +S3_155 +S3_156 +S3_157 +S3_158 +S3_159 +S3_160 +S3_161 +S3_162 +S3_163 +S3_164 +S3_165 +S3_166 +S3_167 +S3_168 +S3_169 +S3_170 +S3_171 +S3_172 +S3_173 +S3_174 +S3_175 +S3_176 +S3_177 +S3_178 +S3_179 +S3_180 diff --git a/sample_data/test_set_1/.placeholder b/sample_data/test_set_1/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/test_set_1/set1_attendance.csv b/sample_data/test_set_1/set1_attendance.csv new file mode 100644 index 0000000..2866da7 --- /dev/null +++ b/sample_data/test_set_1/set1_attendance.csv @@ -0,0 +1,14 @@ +CS101 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050'] + +MAT201 +['S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090'] + +PHY101 +['S001', 'S002', 'S003', 'S004', 'S005', 'S006', 'S007', 'S008', 'S009', 'S010', 'S011', 'S012', 'S013', 'S014', 'S015', 'S016', 'S017', 'S018', 'S019', 'S020', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100', 'S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110', 'S111', 'S112', 'S113', 'S114', 'S115', 'S116', 'S117', 'S118', 'S119', 'S120', 'S121', 'S122', 'S123', 'S124', 'S125', 'S126', 'S127', 'S128', 'S129', 'S130', 'S131', 'S132', 'S133', 'S134', 'S135', 'S136', 'S137', 'S138', 'S139', 'S140', 'S141', 'S142', 'S143', 'S144', 'S145', 'S146', 'S147', 'S148', 'S149', 'S150'] + +ENG101 +['S021', 'S022', 'S023', 'S024', 'S025', 'S026', 'S027', 'S028', 'S029', 'S030', 'S031', 'S032', 'S033', 'S034', 'S035', 'S036', 'S037', 'S038', 'S039', 'S040', 'S041', 'S042', 'S043', 'S044', 'S045', 'S046', 'S047', 'S048', 'S049', 'S050', 'S051', 'S052', 'S053', 'S054', 'S055', 'S056', 'S057', 'S058', 'S059', 'S060', 'S061', 'S062', 'S063', 'S064', 'S065', 'S066', 'S067', 'S068', 'S069', 'S070', 'S071', 'S072', 'S073', 'S074', 'S075', 'S076', 'S077', 'S078', 'S079', 'S080', 'S081', 'S082', 'S083', 'S084', 'S085', 'S086', 'S087', 'S088', 'S089', 'S090', 'S091', 'S092', 'S093', 'S094', 'S095', 'S096', 'S097', 'S098', 'S099', 'S100', 'S101', 'S102', 'S103', 'S104', 'S105', 'S106', 'S107', 'S108', 'S109', 'S110'] + +HIS202 +['S121', 'S122', 'S123', 'S124', 'S125', 'S126', 'S127', 'S128', 'S129', 'S130', 'S131', 'S132', 'S133', 'S134', 'S135', 'S136', 'S137', 'S138', 'S139', 'S140', 'S141', 'S142', 'S143', 'S144', 'S145', 'S146', 'S147', 'S148', 'S149', 'S150'] diff --git a/sample_data/test_set_1/set1_classrooms.csv b/sample_data/test_set_1/set1_classrooms.csv new file mode 100644 index 0000000..dd61f18 --- /dev/null +++ b/sample_data/test_set_1/set1_classrooms.csv @@ -0,0 +1,4 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +C101;50 +C102;50 +C103;100 diff --git a/sample_data/test_set_1/set1_courses.csv b/sample_data/test_set_1/set1_courses.csv new file mode 100644 index 0000000..533b703 --- /dev/null +++ b/sample_data/test_set_1/set1_courses.csv @@ -0,0 +1,6 @@ +ALL OF THE COURSES IN THE SYSTEM +CS101 +MAT201 +PHY101 +ENG101 +HIS202 diff --git a/sample_data/test_set_1/set1_students.csv b/sample_data/test_set_1/set1_students.csv new file mode 100644 index 0000000..f12fd37 --- /dev/null +++ b/sample_data/test_set_1/set1_students.csv @@ -0,0 +1,151 @@ +ALL OF THE STUDENTS IN THE SYSTEM +S001 +S002 +S003 +S004 +S005 +S006 +S007 +S008 +S009 +S010 +S011 +S012 +S013 +S014 +S015 +S016 +S017 +S018 +S019 +S020 +S021 +S022 +S023 +S024 +S025 +S026 +S027 +S028 +S029 +S030 +S031 +S032 +S033 +S034 +S035 +S036 +S037 +S038 +S039 +S040 +S041 +S042 +S043 +S044 +S045 +S046 +S047 +S048 +S049 +S050 +S051 +S052 +S053 +S054 +S055 +S056 +S057 +S058 +S059 +S060 +S061 +S062 +S063 +S064 +S065 +S066 +S067 +S068 +S069 +S070 +S071 +S072 +S073 +S074 +S075 +S076 +S077 +S078 +S079 +S080 +S081 +S082 +S083 +S084 +S085 +S086 +S087 +S088 +S089 +S090 +S091 +S092 +S093 +S094 +S095 +S096 +S097 +S098 +S099 +S100 +S101 +S102 +S103 +S104 +S105 +S106 +S107 +S108 +S109 +S110 +S111 +S112 +S113 +S114 +S115 +S116 +S117 +S118 +S119 +S120 +S121 +S122 +S123 +S124 +S125 +S126 +S127 +S128 +S129 +S130 +S131 +S132 +S133 +S134 +S135 +S136 +S137 +S138 +S139 +S140 +S141 +S142 +S143 +S144 +S145 +S146 +S147 +S148 +S149 +S150 diff --git a/sample_data/test_set_2/.placeholder b/sample_data/test_set_2/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/sample_data/test_set_2/set2_attendance.csv b/sample_data/test_set_2/set2_attendance.csv new file mode 100644 index 0000000..34d1ac3 --- /dev/null +++ b/sample_data/test_set_2/set2_attendance.csv @@ -0,0 +1,11 @@ +EE301 +['S2_001', 'S2_002', 'S2_003', 'S2_004', 'S2_005', 'S2_006', 'S2_007', 'S2_008', 'S2_009', 'S2_010', 'S2_011', 'S2_012', 'S2_013', 'S2_014', 'S2_015', 'S2_016', 'S2_017', 'S2_018', 'S2_019', 'S2_020', 'S2_021', 'S2_022', 'S2_023', 'S2_024', 'S2_025', 'S2_026', 'S2_027', 'S2_028', 'S2_029', 'S2_030', 'S2_031', 'S2_032', 'S2_033', 'S2_034', 'S2_035', 'S2_036', 'S2_037', 'S2_038', 'S2_039', 'S2_040', 'S2_041', 'S2_042', 'S2_043', 'S2_044', 'S2_045', 'S2_046', 'S2_047', 'S2_048', 'S2_049', 'S2_050', 'S2_051', 'S2_052', 'S2_053', 'S2_054', 'S2_055', 'S2_056', 'S2_057', 'S2_058', 'S2_059', 'S2_060', 'S2_061', 'S2_062', 'S2_063', 'S2_064', 'S2_065', 'S2_066', 'S2_067', 'S2_068', 'S2_069', 'S2_070', 'S2_071', 'S2_072', 'S2_073', 'S2_074', 'S2_075'] + +EE302 +['S2_076', 'S2_077', 'S2_078', 'S2_079', 'S2_080', 'S2_081', 'S2_082', 'S2_083', 'S2_084', 'S2_085', 'S2_086', 'S2_087', 'S2_088', 'S2_089', 'S2_090', 'S2_091', 'S2_092', 'S2_093', 'S2_094', 'S2_095', 'S2_096', 'S2_097', 'S2_098', 'S2_099', 'S2_100', 'S2_101', 'S2_102', 'S2_103', 'S2_104', 'S2_105', 'S2_106', 'S2_107', 'S2_108', 'S2_109', 'S2_110', 'S2_111', 'S2_112', 'S2_113', 'S2_114', 'S2_115', 'S2_116', 'S2_117', 'S2_118', 'S2_119', 'S2_120', 'S2_121', 'S2_122', 'S2_123', 'S2_124', 'S2_125', 'S2_126', 'S2_127', 'S2_128', 'S2_129', 'S2_130'] + +IE450 +['S2_101', 'S2_102', 'S2_103', 'S2_104', 'S2_105', 'S2_106', 'S2_107', 'S2_108', 'S2_109', 'S2_110', 'S2_111', 'S2_112', 'S2_113', 'S2_114', 'S2_115', 'S2_116', 'S2_117', 'S2_118', 'S2_119', 'S2_120', 'S2_121', 'S2_122', 'S2_123', 'S2_124', 'S2_125', 'S2_126', 'S2_127', 'S2_128', 'S2_129', 'S2_130', 'S2_131', 'S2_132', 'S2_133', 'S2_134', 'S2_135', 'S2_136', 'S2_137', 'S2_138', 'S2_139', 'S2_140', 'S2_141', 'S2_142', 'S2_143', 'S2_144', 'S2_145', 'S2_146', 'S2_147', 'S2_148', 'S2_149', 'S2_150', 'S2_151', 'S2_152', 'S2_153', 'S2_154', 'S2_155', 'S2_156', 'S2_157', 'S2_158', 'S2_159', 'S2_160', 'S2_161', 'S2_162', 'S2_163', 'S2_164', 'S2_165', 'S2_166', 'S2_167', 'S2_168', 'S2_169', 'S2_170', 'S2_171', 'S2_172', 'S2_173', 'S2_174', 'S2_175', 'S2_176', 'S2_177', 'S2_178', 'S2_179', 'S2_180', 'S2_181', 'S2_182', 'S2_183', 'S2_184', 'S2_185', 'S2_186', 'S2_187', 'S2_188', 'S2_189', 'S2_190', 'S2_191', 'S2_192', 'S2_193', 'S2_194', 'S2_195'] + +CE212 +['S2_161', 'S2_162', 'S2_163', 'S2_164', 'S2_165', 'S2_166', 'S2_167', 'S2_168', 'S2_169', 'S2_170', 'S2_171', 'S2_172', 'S2_173', 'S2_174', 'S2_175', 'S2_176', 'S2_177', 'S2_178', 'S2_179', 'S2_180', 'S2_181', 'S2_182', 'S2_183', 'S2_184', 'S2_185', 'S2_186', 'S2_187', 'S2_188', 'S2_189', 'S2_190', 'S2_191', 'S2_192', 'S2_193', 'S2_194', 'S2_195', 'S2_196', 'S2_197', 'S2_198', 'S2_199', 'S2_200'] diff --git a/sample_data/test_set_2/set2_classrooms.csv b/sample_data/test_set_2/set2_classrooms.csv new file mode 100644 index 0000000..6958454 --- /dev/null +++ b/sample_data/test_set_2/set2_classrooms.csv @@ -0,0 +1,5 @@ +ALL OF THE CLASSROOMS; AND THEIR CAPACITIES IN THE SYSTEM +D-Z01;40 +D-Z02;40 +D-101;60 +D-102;60 diff --git a/sample_data/test_set_2/set2_courses.csv b/sample_data/test_set_2/set2_courses.csv new file mode 100644 index 0000000..5bc67f5 --- /dev/null +++ b/sample_data/test_set_2/set2_courses.csv @@ -0,0 +1,5 @@ +ALL OF THE COURSES IN THE SYSTEM +EE301 +EE302 +IE450 +CE212 diff --git a/sample_data/test_set_2/set2_students.csv b/sample_data/test_set_2/set2_students.csv new file mode 100644 index 0000000..43fb3ab --- /dev/null +++ b/sample_data/test_set_2/set2_students.csv @@ -0,0 +1,201 @@ +ALL OF THE STUDENTS IN THE SYSTEM +S2_001 +S2_002 +S2_003 +S2_004 +S2_005 +S2_006 +S2_007 +S2_008 +S2_009 +S2_010 +S2_011 +S2_012 +S2_013 +S2_014 +S2_015 +S2_016 +S2_017 +S2_018 +S2_019 +S2_020 +S2_021 +S2_022 +S2_023 +S2_024 +S2_025 +S2_026 +S2_027 +S2_028 +S2_029 +S2_030 +S2_031 +S2_032 +S2_033 +S2_034 +S2_035 +S2_036 +S2_037 +S2_038 +S2_039 +S2_040 +S2_041 +S2_042 +S2_043 +S2_044 +S2_045 +S2_046 +S2_047 +S2_048 +S2_049 +S2_050 +S2_051 +S2_052 +S2_053 +S2_054 +S2_055 +S2_056 +S2_057 +S2_058 +S2_059 +S2_060 +S2_061 +S2_062 +S2_063 +S2_064 +S2_065 +S2_066 +S2_067 +S2_068 +S2_069 +S2_070 +S2_071 +S2_072 +S2_073 +S2_074 +S2_075 +S2_076 +S2_077 +S2_078 +S2_079 +S2_080 +S2_081 +S2_082 +S2_083 +S2_084 +S2_085 +S2_086 +S2_087 +S2_088 +S2_089 +S2_090 +S2_091 +S2_092 +S2_093 +S2_094 +S2_095 +S2_096 +S2_097 +S2_098 +S2_099 +S2_100 +S2_101 +S2_102 +S2_103 +S2_104 +S2_105 +S2_106 +S2_107 +S2_108 +S2_109 +S2_110 +S2_111 +S2_112 +S2_113 +S2_114 +S2_115 +S2_116 +S2_117 +S2_118 +S2_119 +S2_120 +S2_121 +S2_122 +S2_123 +S2_124 +S2_125 +S2_126 +S2_127 +S2_128 +S2_129 +S2_130 +S2_131 +S2_132 +S2_133 +S2_134 +S2_135 +S2_136 +S2_137 +S2_138 +S2_139 +S2_140 +S2_141 +S2_142 +S2_143 +S2_144 +S2_145 +S2_146 +S2_147 +S2_148 +S2_149 +S2_150 +S2_151 +S2_152 +S2_153 +S2_154 +S2_155 +S2_156 +S2_157 +S2_158 +S2_159 +S2_160 +S2_161 +S2_162 +S2_163 +S2_164 +S2_165 +S2_166 +S2_167 +S2_168 +S2_169 +S2_170 +S2_171 +S2_172 +S2_173 +S2_174 +S2_175 +S2_176 +S2_177 +S2_178 +S2_179 +S2_180 +S2_181 +S2_182 +S2_183 +S2_184 +S2_185 +S2_186 +S2_187 +S2_188 +S2_189 +S2_190 +S2_191 +S2_192 +S2_193 +S2_194 +S2_195 +S2_196 +S2_197 +S2_198 +S2_199 +S2_200 diff --git a/src/app.tsx b/src/app.tsx index 05509c2..9416cc0 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -40,7 +40,7 @@ const App: React.FC = () => { }))); setClassrooms(savedClassrooms.map((r: any) => ({ - id: r.name, + id: r.id.toString(), name: r.name, capacity: r.capacity, building: r.building @@ -62,6 +62,7 @@ const App: React.FC = () => { classroomId: s.classroom_name, startTime: new Date(s.start_time), endTime: new Date(s.end_time), + studentCount: s.student_count, })); setSchedule(loadedSchedule); setIsGenerated(true); @@ -155,6 +156,7 @@ const App: React.FC = () => { classroomName: s.classroomId, startTime: s.startTime, endTime: s.endTime, + studentCount: s.studentCount, })); await window.api.saveSchedule(sessionsForSave); @@ -198,6 +200,9 @@ const App: React.FC = () => { courses={courses} setCourses={setCourses} classrooms={classrooms} setClassrooms={setClassrooms} students={students} setStudents={setStudents} + setSchedule={setSchedule} + + /> ); case ViewMode.SCHEDULE: diff --git a/src/backend/database/db.ts b/src/backend/database/db.ts index b705af7..69523cf 100644 --- a/src/backend/database/db.ts +++ b/src/backend/database/db.ts @@ -43,6 +43,7 @@ db.exec(` session_id TEXT NOT NULL, course_code TEXT NOT NULL, classroom_name TEXT NOT NULL, + student_count INTEGER DEFAULT 0, start_time TEXT NOT NULL, end_time TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP diff --git a/src/backend/handlers/classroomHandler.ts b/src/backend/handlers/classroomHandler.ts index 50501cd..9d7d718 100644 --- a/src/backend/handlers/classroomHandler.ts +++ b/src/backend/handlers/classroomHandler.ts @@ -7,6 +7,7 @@ export const registerClassroomHandlers = () => { }); ipcMain.handle('add-classrooms-bulk', (_, classrooms: { name: string; capacity: number; building: string }[]) => { + // Now this handler will only add new classrooms without clearing existing ones. return classroomService.addClassroomsBulk(classrooms); }); diff --git a/src/backend/services/scheduleService.ts b/src/backend/services/scheduleService.ts index 5fbc64f..410c335 100644 --- a/src/backend/services/scheduleService.ts +++ b/src/backend/services/scheduleService.ts @@ -11,6 +11,7 @@ export interface ExamSessionDB { start_time: string; end_time: string; created_at: string; + student_count: number; } export interface ExamSessionInput { @@ -19,6 +20,7 @@ export interface ExamSessionInput { classroomName: string; startTime: string; endTime: string; + studentCount?: number; } /** @@ -31,8 +33,8 @@ export const saveSchedule = (sessions: ExamSessionInput[]): void => { // Insert new sessions const insert = db.prepare(` - INSERT INTO exam_sessions (session_id, course_code, classroom_name, start_time, end_time) - VALUES (@sessionId, @courseCode, @classroomName, @startTime, @endTime) + INSERT INTO exam_sessions (session_id, course_code, classroom_name, start_time, end_time, student_count) + VALUES (@sessionId, @courseCode, @classroomName, @startTime, @endTime, @studentCount) `); const insertMany = db.transaction((sessionsList: ExamSessionInput[]) => { @@ -46,6 +48,7 @@ export const saveSchedule = (sessions: ExamSessionInput[]): void => { endTime: typeof session.endTime === 'string' ? session.endTime : (session.endTime as unknown as Date).toISOString(), + studentCount: session.studentCount || 0 }; insert.run(sessionData); } @@ -88,11 +91,11 @@ export const exportScheduleToCSV = async ( ): Promise<{ success: boolean; filePath?: string; message: string }> => { try { // Create course and classroom lookup maps - const courseMap = new Map(courses.map(c => [c.id, c])); - const classroomMap = new Map(classrooms.map(c => [c.id, c])); + const courseMap = new Map(courses.map(c => [c.code, c])); + const classroomMap = new Map(classrooms.map(c => [c.name, c])); // Build CSV content - const headers = ['Exam ID', 'Course Code', 'Course Name', 'Classroom', 'Date', 'Start Time', 'End Time']; + const headers = ['Exam ID', 'Course Code', 'Course Name', 'Classroom', 'Student Count', 'Date', 'Start Time', 'End Time']; const rows = sessions.map(session => { const course = courseMap.get(session.courseCode); const classroom = classroomMap.get(session.classroomName); @@ -104,6 +107,7 @@ export const exportScheduleToCSV = async ( course?.code || session.courseCode, course?.name || 'Unknown', classroom?.name || session.classroomName, + session.studentCount?.toString() || '0', startDate.toLocaleDateString('en-US'), startDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }), endDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }) diff --git a/src/backend/services/schedulerService.test.ts b/src/backend/services/schedulerService.test.ts index bd459af..2c4dac8 100644 --- a/src/backend/services/schedulerService.test.ts +++ b/src/backend/services/schedulerService.test.ts @@ -514,3 +514,54 @@ describe('Scheduler - Validation', () => { expect(valResult.violations.length).toBeGreaterThan(0); }); }); + +// ============================================================================ +// 10. Split Logic Tests +// ============================================================================ + +describe('Scheduler - Split Logic', () => { + test('should split a large course across multiple rooms', () => { + const courses = [createCourse('C1', 150)]; + const classrooms = [ + createClassroom('R1', 100), + createClassroom('R2', 50), + ]; + const students = Array.from({ length: 150 }, (_, i) => createStudent(`S${i}`, ['C1'])); + const constraints = createConstraints(); + + const result = generateSchedule(courses, classrooms, students, constraints); + + expect(result.success).toBe(true); + expect(result.schedule).toHaveLength(2); // One course split into two sessions + + // Check that both rooms are used at the same time for the same course + expect(result.schedule[0].courseId).toBe('C1'); + expect(result.schedule[1].courseId).toBe('C1'); + expect(result.schedule[0].startTime.getTime()).toBe(result.schedule[1].startTime.getTime()); + + const usedRoomIds = new Set(result.schedule.map(s => s.classroomId)); + expect(usedRoomIds).toContain('R1'); + expect(usedRoomIds).toContain('R2'); + }); + + test('should not use non-existent classrooms when splitting', () => { + const courses = [createCourse('C-SPLIT-101', 120)]; + const classrooms = [ + createClassroom('REAL-ROOM-1', 80), + createClassroom('REAL-ROOM-2', 40), + ]; + const students = Array.from({ length: 120 }, (_, i) => createStudent(`S${i}`, ['C-SPLIT-101'])); + const constraints = createConstraints(); + + const result = generateSchedule(courses, classrooms, students, constraints); + + expect(result.success).toBe(true); + expect(result.schedule.length).toBeGreaterThanOrEqual(2); + + const originalClassroomIds = new Set(classrooms.map(c => c.id)); + + for (const session of result.schedule) { + expect(originalClassroomIds.has(session.classroomId)).toBe(true); + } + }); +}); \ No newline at end of file diff --git a/src/backend/services/schedulerService.ts b/src/backend/services/schedulerService.ts index 9b2afbe..ab66e4c 100644 --- a/src/backend/services/schedulerService.ts +++ b/src/backend/services/schedulerService.ts @@ -24,6 +24,8 @@ interface ScheduleAssignment { course: Course; classroom: Classroom; timeSlot: TimeSlot; + studentCount:number; + } /** @@ -232,9 +234,8 @@ function isSafe( ): boolean { // Constraint 1: Capacity Check // Room must have enough capacity for all enrolled students - if (classroom.capacity < course.enrolledStudents) { - return false; - } + + // Constraint 2: Room Availability Check // The classroom must not be already booked at this time @@ -252,6 +253,7 @@ function isSafe( // Constraint 3: No Student Conflicts // A student cannot have two exams at the same time slot const sameTimeConflict = context.assignments.some((a) => { + if (a.course.id === course.id) return false; const studentsInAssignedCourse = context.courseStudentMap.get(a.course.id) || []; return studentsInAssignedCourse.includes(studentId) && a.timeSlot.id === timeSlot.id; }); @@ -440,21 +442,42 @@ function getFailureReason( let rootCause = ''; let rootCauseType: ConstraintViolation['type'] = 'capacity'; - // Priority 1: Capacity issue (no room can fit) - if (roomsWithCapacity.length === 0) { - rootCause = `NO ROOM AVAILABLE: Course has ${course.enrolledStudents} students but largest room only has ${maxRoomCapacity} seats.`; + // NEW Priority 1: Insufficient TOTAL capacity across rooms, even with splitting + let hasAnySlotEnoughTotalCapacity = false; + let maxPossibleCapacityInAnySingleSlot = 0; + + for (const timeSlot of timeSlots) { + const occupiedRoomIdsAtSlot = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + const totalUnoccupiedCapacityAtSlot = classrooms + .filter(r => !occupiedRoomIdsAtSlot.has(r.id)) + .reduce((sum, room) => sum + room.capacity, 0); + + if (totalUnoccupiedCapacityAtSlot >= course.enrolledStudents) { + hasAnySlotEnoughTotalCapacity = true; + break; + } + maxPossibleCapacityInAnySingleSlot = Math.max(maxPossibleCapacityInAnySingleSlot, totalUnoccupiedCapacityAtSlot); + } + + if (!hasAnySlotEnoughTotalCapacity) { + rootCause = `INSUFFICIENT TOTAL CAPACITY: Course has ${course.enrolledStudents} students. No single time slot has enough total available classroom capacity (max ${maxPossibleCapacityInAnySingleSlot}) to accommodate all students, even with splitting across multiple rooms.`; rootCauseType = 'capacity'; violations.push({ type: 'capacity', severity: 'blocking', - description: `Need ${course.enrolledStudents - maxRoomCapacity} more seats. No room is large enough.`, + description: `Required total capacity: ${course.enrolledStudents}. Maximum available total capacity in any single time slot: ${maxPossibleCapacityInAnySingleSlot}.`, details: { requiredCapacity: course.enrolledStudents, - maxAvailableCapacity: maxRoomCapacity + maxAvailableCapacity: maxPossibleCapacityInAnySingleSlot } }); } - // Priority 2: Student conflicts blocking all slots + // Original Priority 1 becomes Priority 2 if the new total capacity check passes else if (studentConflictMap.size > 0) { const topConflict = [...studentConflictMap.entries()].sort((a, b) => b[1].students.size - a[1].students.size)[0]; const [conflictCourse, data] = topConflict; @@ -522,12 +545,12 @@ function getFailureReason( } // Fallback: General resource constraint else { - rootCause = `RESOURCE EXHAUSTED: All ${timeSlots.length} slots × ${roomsWithCapacity.length} rooms are occupied or blocked.`; + rootCause = `RESOURCE EXHAUSTED: All time/room combinations are exhausted for ${course.code}.`; rootCauseType = 'room_conflict'; violations.push({ type: 'room_conflict', severity: 'blocking', - description: 'All available time/room combinations are already used', + description: 'All available time/room combinations are already used or blocked by other constraints', details: { blockedSlots: slotsBlockedByRoomConflicts } }); } @@ -538,9 +561,16 @@ function getFailureReason( if (rootCauseType === 'capacity') { suggestions.push({ priority: 'high', - action: `Add a room with ${course.enrolledStudents}+ seats`, - impact: `Will immediately allow scheduling ${course.code}` + action: `Add more classrooms or increase capacity of existing ones.`, + impact: `Will immediately provide more total seating capacity for ${course.code}` }); + if (maxPossibleCapacityInAnySingleSlot > 0) { + suggestions.push({ + priority: 'medium', + action: `Consider reducing enrollment for ${course.code} by ${course.enrolledStudents - maxPossibleCapacityInAnySingleSlot} students.`, + impact: `May allow course to fit into existing capacity.` + }); + } } if (rootCauseType === 'student_conflict' && studentConflictMap.size > 0) { @@ -624,6 +654,15 @@ function getFailureReason( * @param maxIterations - Maximum allowed iterations before timeout * @returns true if a valid schedule was found, false otherwise */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + */ +/** + * Recursive Backtracking Solver with SPLIT SUPPORT. + * Tries to schedule courses by splitting them across multiple rooms if necessary. + * Strategy: Largest-Fit (Fills largest available rooms first). + */ function solve( courses: Course[], classrooms: Classroom[], @@ -633,49 +672,113 @@ function solve( startTime: number, timeoutMs: number ): boolean { - // Check for timeout (time-based, not iteration-based) - if (Date.now() - startTime > timeoutMs) { - return false; // Stop trying - timeout reached - } - - // Base case: All courses have been scheduled - if (index === courses.length) { - return true; - } + // 1. Timeout ve Base Case Kontrolleri + if (Date.now() - startTime > timeoutMs) return false; + if (index === courses.length) return true; // Tüm dersler atandıysa başarı! const currentCourse = courses[index]; + let studentsRemainingForCurrentCourse = currentCourse.enrolledStudents; - // Try each time slot + // 2. Her zaman dilimini sırayla dene for (const timeSlot of timeSlots) { - // Check for timeout in inner loop - if (Date.now() - startTime > timeoutMs) { - return false; + // Döngü içi timeout kontrolü (Büyük veride donmayı önler) + if (Date.now() - startTime > timeoutMs) return false; + + // 3. Önce Öğrenci Çakışmasını Kontrol Et (Optimization) + // Eğer öğrencilerin bu saatte başka sınavı varsa, boş oda aramaya gerek yok. + // Not: classrooms[0] dummy olarak verilir, isSafe içinde oda ID'sine bakılmaz (split için). + if (!isSafe(currentCourse, classrooms[0], timeSlot, context)) { + continue; } - // Try each classroom - for (const classroom of classrooms) { - // Check if this assignment satisfies all constraints - if (isSafe(currentCourse, classroom, timeSlot, context)) { - // Make the assignment - const assignment: ScheduleAssignment = { + // 4. Bu saatteki Müsait Odaları Bul + // Daha önce atanmış (dolu) odaların ID'lerini al + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları filtrele ve KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala + // Bu "Largest Fit" stratejisidir. 150 kişilik ders için önce 100'lük, sonra 50'lik odayı seçer. + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) // Only consider rooms that are not occupied + .sort((a, b) => b.capacity - a.capacity); // Sort by capacity descending (Largest Fit) + + // 5. Dersi Odalara Bölüştür (Split Logic) + let studentsRemaining = studentsRemainingForCurrentCourse; // Use a local variable for splitting logic + const potentialAssignments: ScheduleAssignment[] = []; + const roomsUsedInCurrentSplit = new Set(); // Keep track of rooms already used for this course split + + while (studentsRemaining > 0) { + // Get rooms that are available (not occupied by another course in this time slot) + // and have not yet been used for *this* course's split. + const currentAvailableRooms = classrooms.filter(r => + !occupiedRoomIds.has(r.id) && !roomsUsedInCurrentSplit.has(r.id) + ); + + if (currentAvailableRooms.length === 0) { + studentsRemaining = -1; // Indicate failure to place all students + break; + } + + // --- Strategy: Prioritize Best Fit for remaining students --- + let bestFitRoom: Classroom | null = null; + let bestFitCapacityDiff = Infinity; // capacity - studentsRemaining, want smallest non-negative difference + + for (const room of currentAvailableRooms) { + if (room.capacity >= studentsRemaining) { // Can this room fit all remaining students? + const diff = room.capacity - studentsRemaining; + if (diff < bestFitCapacityDiff) { // Is it a better fit (smaller difference)? + bestFitCapacityDiff = diff; + bestFitRoom = room; + } + } + } + + if (bestFitRoom) { // Found a room that can fit all remaining students (Best Fit) + potentialAssignments.push({ course: currentCourse, - classroom, - timeSlot - }; + classroom: bestFitRoom, + timeSlot: timeSlot, + studentCount: studentsRemaining + }); + roomsUsedInCurrentSplit.add(bestFitRoom.id); + studentsRemaining = 0; // All students placed + } else { // No single room can fit all remaining students, use Largest Fit strategy for current chunk + // Find the largest available and unused room + const largestRoom = currentAvailableRooms.sort((a, b) => b.capacity - a.capacity)[0]; + + const count = Math.min(studentsRemaining, largestRoom.capacity); + potentialAssignments.push({ + course: currentCourse, + classroom: largestRoom, + timeSlot: timeSlot, + studentCount: count + }); + roomsUsedInCurrentSplit.add(largestRoom.id); + studentsRemaining -= count; + } + } + + + if (studentsRemaining === 0) { + + for (const assignment of potentialAssignments) { context.assignments.push(assignment); + } - // Recursively try to schedule the remaining courses - if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { - return true; - } + + if (solve(courses, classrooms, timeSlots, context, index + 1, startTime, timeoutMs)) { + return true; + } - // Backtrack: Remove the assignment and try another option + for (let i = 0; i < potentialAssignments.length; i++) { context.assignments.pop(); } } } - // No valid assignment found for this course return false; } @@ -685,53 +788,115 @@ function solve( * O(courses * timeSlots * classrooms) - much faster than backtracking. * Returns both success status and detailed failure reason if failed. */ +/** + * Greedy scheduling algorithm with SPLIT SUPPORT. + * Tries to fit a course into one OR MORE classrooms if needed. + */ function solveGreedy( courses: Course[], classrooms: Classroom[], timeSlots: TimeSlot[], context: ValidationContext ): { success: boolean; failureReason?: FailureReason } { - console.log(`Greedy solver: ${courses.length} courses, ${timeSlots.length} slots, ${classrooms.length} rooms`); + console.log(`Greedy solver (Split-Enabled): ${courses.length} courses, ${timeSlots.length} slots.`); for (let i = 0; i < courses.length; i++) { const course = courses[i]; - let assigned = false; + let placed = false; - // Try each time slot + // Her zaman dilimini dene for (const timeSlot of timeSlots) { - if (assigned) break; - - // Try each classroom - for (const classroom of classrooms) { - if (isSafe(course, classroom, timeSlot, context)) { - // Make the assignment - context.assignments.push({ - course, - classroom, - timeSlot - }); - assigned = true; + if (placed) break; + + // 1. ADIM: Öğrenci çakışması (Student Conflict) var mı? + // Herhangi bir odada bu dersi yapabilir miyiz diye genel bir kontrol yapalım. + // Not: İlk odayı referans alıyoruz çünkü isSafe içindeki öğrenci kontrolü odadan bağımsızdır. + if (!isSafe(course, classrooms[0], timeSlot, context)) { + continue; // Öğrencilerin bu saatte başka sınavı var, bu saati geç. + } + + // 2. ADIM: Bu saatteki BOŞ odaları bul + const occupiedRoomIds = new Set( + context.assignments + .filter(a => a.timeSlot.id === timeSlot.id) + .map(a => a.classroom.id) + ); + + // Boş odaları KAPASİTEYE göre BÜYÜKTEN KÜÇÜĞE sırala (Largest Fit) + const availableRooms = classrooms + .filter(r => !occupiedRoomIds.has(r.id)) + .sort((a, b) => b.capacity - a.capacity); + + // 3. ADIM: Öğrencileri odalara dağıt (SPLIT MANTIĞI) + let studentsRemaining = course.enrolledStudents; + const roomsToUse: Classroom[] = []; + const roomsUsedInCurrentSplit = new Set(); + + while (studentsRemaining > 0) { + const currentAvailableRooms = classrooms.filter(r => + !occupiedRoomIds.has(r.id) && !roomsUsedInCurrentSplit.has(r.id) + ); + + if (currentAvailableRooms.length === 0) { + studentsRemaining = -1; // Indicate failure break; } + + // --- Strategy: Prioritize Best Fit for remaining students --- + let bestFitRoom: Classroom | null = null; + let bestFitCapacityDiff = Infinity; + + for (const room of currentAvailableRooms) { + if (room.capacity >= studentsRemaining) { + const diff = room.capacity - studentsRemaining; + if (diff < bestFitCapacityDiff) { + bestFitCapacityDiff = diff; + bestFitRoom = room; + } + } + } + + if (bestFitRoom) { // Found a room that can fit all remaining students (Best Fit) + roomsToUse.push(bestFitRoom); + roomsUsedInCurrentSplit.add(bestFitRoom.id); + studentsRemaining = 0; // All students placed + } else { // No single room can fit all remaining students, use Largest Fit strategy + const largestRoom = currentAvailableRooms.sort((a, b) => b.capacity - a.capacity)[0]; + + roomsToUse.push(largestRoom); + roomsUsedInCurrentSplit.add(largestRoom.id); + studentsRemaining -= largestRoom.capacity; + } + } + + // 4. ADIM: Eğer tüm öğrenciler yerleştiyse (yani odalar yettiyse), atamayı yap + if (studentsRemaining === 0) { + let studentsToAssign = course.enrolledStudents; + roomsToUse.forEach(room => { + const count = Math.min(studentsToAssign, room.capacity); + if (count > 0) { + context.assignments.push({ + course: course, + classroom: room, + timeSlot: timeSlot, + studentCount: count + }); + studentsToAssign -= count; + } + }); + placed = true; } } - if (!assigned) { - // Could not find a valid slot for this course - analyze why + if (!placed) { const failureReason = getFailureReason(course, classrooms, timeSlots, context, i, courses.length); - console.log(`FAILED at course ${i + 1}/${courses.length}: ${course.code} (${course.enrolledStudents} students)`); - console.log(` Root cause: ${failureReason.rootCause}`); + console.log(`FAILED at course ${course.code}: Not enough room capacity (even with splitting) or time slots.`); return { success: false, failureReason }; } - - // Progress log every 20 courses - if ((i + 1) % 20 === 0) { - console.log(` Progress: ${i + 1}/${courses.length} courses scheduled`); - } } console.log(`SUCCESS: All ${courses.length} courses scheduled!`); - return { success: true }; // All courses scheduled + return { success: true }; } // ============================================================================ @@ -753,6 +918,7 @@ export function generateSchedule( students: Student[], constraints: GenerationConstraints ): ScheduleResult { + console.log('SCHEDULER INPUT CLASSROOMS:', JSON.stringify(classrooms, null, 2)); const startTime = Date.now(); // Validate input @@ -837,6 +1003,7 @@ export function generateSchedule( allowConsecutiveExams: constraints.allowConsecutiveExams ?? true, minHoursBetweenExams: constraints.minHoursBetweenExams ?? 1 }; + // Choose algorithm based on dataset size // Greedy: Fast, good for large datasets (50+ courses) @@ -929,11 +1096,14 @@ export function generateSchedule( const schedule: ExamSession[] = context.assignments.map((a, idx) => ({ id: `exam_${idx + 1}`, courseId: a.course.id, - classroomId: a.classroom.name, + classroomId: a.classroom.id, startTime: a.timeSlot.startTime, - endTime: a.timeSlot.endTime + endTime: a.timeSlot.endTime, + studentCount: a.studentCount })); + console.log('SCHEDULER OUTPUT SCHEDULE:', JSON.stringify(schedule, null, 2)); + return { success: true, schedule, diff --git a/src/components/DataInput.tsx b/src/components/DataInput.tsx index a1055d8..9f59b21 100644 --- a/src/components/DataInput.tsx +++ b/src/components/DataInput.tsx @@ -10,6 +10,7 @@ interface DataInputProps { setClassrooms: React.Dispatch>; students: Student[]; setStudents: React.Dispatch>; + setSchedule: React.Dispatch>; } type Tab = 'courses' | 'classrooms' | 'students'|'enrollments'; @@ -331,10 +332,20 @@ export const DataInput: React.FC = ({ if (window.confirm(confirmMessage)) { if (type === 'all') { + // 1. Clear input data from DB await window.api.clearCourses(); await window.api.clearClassrooms(); await window.api.clearStudents(); - setCourses([]); setClassrooms([]); setStudents([]); + await window.api.clearSchedule(); + + // 2. Update frontend state + setCourses([]); + setClassrooms([]); + setStudents([]); + // Assuming this setter is for the schedule data + + showNotification("All data and generated schedules have been wiped.", 'success'); + } else if (type === 'courses') { await window.api.clearCourses(); setCourses([]); @@ -348,25 +359,37 @@ export const DataInput: React.FC = ({ setIsClearMenuOpen(false); } }; - // Helper function to detect the type of the imported file based on its content + // Detects file type based on specific header strings or formatting + + // Helper to distinguish between standard Student lists and Attendance/Enrollment lists + const detectFileType = (text: string): string => { - const cleanText = text.trim(); - if (cleanText.includes('ALL OF THE COURSES')) return 'courses'; - if (cleanText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; - if (cleanText.includes('ALL OF THE STUDENTS')) return 'students'; - // Check for the specific Python-like list format: [ 'STD001', ... ] - if (cleanText.includes('[') && cleanText.includes(']')) return 'enrollments'; + const upperText = text.toUpperCase(); + + // Student Info files always contain this header + if (upperText.includes('ALL OF THE STUDENTS')) return 'students'; + + // Course files header + if (upperText.includes('ALL OF THE COURSES')) return 'courses'; + + // Classroom files header + if (upperText.includes('ALL OF THE CLASSROOMS')) return 'classrooms'; + + // Attendance/Enrollment files contain the list format like [ 'ID1', 'ID2' ] + if (text.includes('[') && text.includes(']')) return 'enrollments'; + return 'unknown'; }; - const handleAttendanceFileChange = (e: React.ChangeEvent) => { + +const handleAttendanceFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - // CONSTRAINT: Prevent import if primary data (courses/students) is missing + // CONSTRAINT: Prevent import if Courses or Students (in-app or temporary) are missing const studentsAvailable = students.length > 0 || tempStudentMap.current.size > 0; if (courses.length === 0 || !studentsAvailable) { - showNotification("Dependency Error: Please import Courses and student information first!", 'error'); + showNotification("Dependency Error: Please import Courses and student information before importing attendance!", 'error'); e.target.value = ''; return; } @@ -375,9 +398,8 @@ export const DataInput: React.FC = ({ reader.onload = async (event) => { const text = event.target?.result as string; - // VALIDATION: Check if the file content matches the Enrollment format - const detected = detectFileType(text); - if (detected !== 'enrollments') { + // VALIDATION: Ensure the user is uploading an actual Enrollment file + if (detectFileType(text) !== 'enrollments') { showNotification("Format Error: This file is not a valid attendance list file!", 'error'); return; } @@ -385,11 +407,36 @@ export const DataInput: React.FC = ({ try { const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); const map = extractAttendanceMap(rows); + + // Combine students from app state and the temporary import buffer + const allAvailableStudentIds = new Set(students.map(s => s.id.toUpperCase())); + tempStudentMap.current.forEach((_name, id) => { + allAvailableStudentIds.add(id.toUpperCase()); + }); + + let matchCount = 0; + let skipCount = 0; + map.forEach((val, key) => { - if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); - val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + if (allAvailableStudentIds.has(key.toUpperCase())) { + if (!tempAttendanceMap.current.has(key)) tempAttendanceMap.current.set(key, new Set()); + val.forEach(c => tempAttendanceMap.current.get(key)?.add(c)); + matchCount++; + } else { + skipCount++; + } }); - setAttendanceStatus({ fileName: file.name, count: map.size }); + + if (matchCount > 0) { + setAttendanceStatus({ fileName: file.name, count: matchCount }); + if (skipCount > 0) { + showNotification(`Identified ${matchCount} valid students. Ignored ${skipCount} unknown students.`, 'success'); + } else { + showNotification(`Identified ${matchCount} valid students.`, 'success'); + } + } else { + showNotification("Import Warning: No matching students found from the attendance file.", 'error'); + } } catch (err) { showNotification("Parse Error: Failed to analyze the attendance file.", 'error'); } @@ -410,6 +457,7 @@ export const DataInput: React.FC = ({ fileInputRef.current?.click(); } }; + const handleStudentInfoImport = () => { // Do not close modal here anymore @@ -488,55 +536,65 @@ export const DataInput: React.FC = ({ const parseCourseListFile = async (rows: string[]) => { - const newCourses: Course[] = []; + const newCourses: { id: string, code: string, name: string, enrolledStudents: number }[] = []; + rows.forEach((row) => { if (row.includes("ALL OF THE COURSES") || !row.trim()) return; const code = row.trim(); - newCourses.push({ id: code, code: code, name: `Course ${code}`, enrolledStudents: 0 }); + newCourses.push({ + id: code, + code: code, + name: `Course ${code}`, + enrolledStudents: 0, + }); }); if (newCourses.length > 0) { await window.api.addCoursesBulk(newCourses); - // Refresh from DB to be sure or just update state - const savedCourses = await window.api.getCourses(); - // Map DB result to frontend type if needed, but they are similar. - // DB has `enrolled_students`, frontend `enrolledStudents`. - // We need to map it. - const mappedCourses = savedCourses.map((c: any) => ({ - id: c.code, // Use code as ID for frontend consistency or c.id? Frontend uses string ID often. - // The current frontend uses `code` as `id` for courses often. - // Let's check `Course` type. - // In `types/index.ts` (not seen but inferred), `id` is likely string or number. - // In `DataInput.tsx` line 211: `id` is `code`. - // So let's keep using code as ID for now or map properly. + // After bulk adding, get the complete fresh list from the DB + const allSavedCourses = await window.api.getCourses(); + // Map it correctly + const mappedCourses = allSavedCourses.map((c: any) => ({ + id: c.code, code: c.code, name: c.name, enrolledStudents: c.enrolled_students })); - - setCourses(prev => { - const existingIds = new Set(prev.map(c => c.id)); - const uniqueNew = mappedCourses.filter((c: any) => !existingIds.has(c.id)); - return [...prev, ...uniqueNew]; - }); + // Replace the entire state + setCourses(mappedCourses); showNotification(t('dataInput.importedCourses', { count: newCourses.length }), 'success'); } }; const parseClassroomFile = async (rows: string[]) => { - const newRooms: Classroom[] = []; + const newRooms: { name: string; capacity: number; building: string }[] = []; + rows.forEach((row) => { if (row.includes("ALL OF THE CLASSROOMS") || !row.trim()) return; const parts = row.split(';'); if (parts.length >= 2) { const name = parts[0].trim(); const capacity = parseInt(parts[1].trim(), 10); - newRooms.push({ id: name, name: name, capacity: isNaN(capacity) ? 0 : capacity, building: 'Main Hall' }); + + newRooms.push({ + name: name, + capacity: isNaN(capacity) ? 0 : capacity, + building: 'Main Hall' + }); } }); if (newRooms.length > 0) { await window.api.addClassroomsBulk(newRooms); - const saved = await window.api.getClassrooms(); - setClassrooms(saved.map((r: any) => ({ id: r.name, name: r.name, capacity: r.capacity, building: r.building }))); + // After bulk adding, get the complete fresh list from the DB + const allSavedRooms = await window.api.getClassrooms(); + // Map it correctly + const mappedRooms = allSavedRooms.map((r: any) => ({ + id: r.id.toString(), // The correct mapping + name: r.name, + capacity: r.capacity, + building: r.building + })); + // Replace the entire state, not just append + setClassrooms(mappedRooms); showNotification(t('dataInput.importedClassrooms', { count: newRooms.length }), 'success'); } }; @@ -642,8 +700,116 @@ export const DataInput: React.FC = ({ }; + // Updated to use the correct logic but immediately save (same behavior as before but cleaner) + const parseSimpleStudentList = async (rows: string[]): Promise => { + const map = extractStudentMap(rows); + // We need to merge this with potentially existing data? + // processCSV is usually for bulk import of everything. + // For safely: we just save what we found. + const newStudents: any[] = []; + map.forEach((name, id) => { + newStudents.push({ + studentNumber: id, + name: name, + enrolledCourses: [] + }); + }); - // --- Parsing Wrappers for ProcessCSV (Legacy/Direct) --- + if (newStudents.length > 0) { + await window.api.addStudentsBulk(newStudents); + // Refresh... + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + showNotification(t('dataInput.importedStudents', { count: newStudents.length }), 'success'); + return newStudents.length; + } + return 0; + }; + // Refreshes local state from the database to ensure UI is up to date + const refreshSystemData = async () => { + const savedStudents = await window.api.getStudents(); + setStudents(savedStudents.map((s: any) => ({ + id: s.student_number, + name: s.name, + email: `${s.student_number.toLowerCase()}@uni.edu`, + enrolledCourses: s.enrolled_courses + }))); + + const savedCourses = await window.api.getCourses(); + setCourses(savedCourses.map((c: any) => ({ + id: c.code, + code: c.code, + name: c.name, + enrolledStudents: c.enrolled_students + }))); + }; + + // Processes the attendance file and links ONLY existing students to existing courses + // Processes attendance by matching file data against existing system data + const parseStudentAttendanceFile = async (rows: string[]): Promise => { + const rawMap = extractAttendanceMap(rows); + const validEnrollments: any[] = []; + + // Create lookup sets for O(1) performance + const existingStudentIds = new Set(students.map(s => s.id.toUpperCase())); + const existingCourseCodes = new Set(courses.map(c => c.code.toUpperCase())); + + let totalFoundInFile = 0; + let matchedCount = 0; + let missingStudentsCount = 0; + + rawMap.forEach((courseCodes, studentId) => { + const normalizedId = studentId.toUpperCase(); + totalFoundInFile++; + + // CHECK: Does this student exist in the Students tab? + if (existingStudentIds.has(normalizedId)) { + // Filter courses: only keep those that exist in the Courses tab + const validCourses = Array.from(courseCodes).filter(code => + existingCourseCodes.has(code.toUpperCase()) + ); + + if (validCourses.length > 0) { + validEnrollments.push({ + studentNumber: studentId, + name: "", // Leave empty; backend won't update existing names + enrolledCourses: validCourses + }); + matchedCount++; + } + } else { + // Student not found in system - increment skip counter + missingStudentsCount++; + } + }); + + // If we found at least one match, proceed with the import + if (validEnrollments.length > 0) { + await window.api.addStudentsBulk(validEnrollments); + + // Notify user about the partial success and the missing students + if (missingStudentsCount > 0) { + showNotification( + `Matched ${matchedCount} students. Records not found for ${missingStudentsCount} students in the list.`, + 'success' + ); + } else { + showNotification(`Successfully matched all ${matchedCount} students.`, 'success'); + } + + // Refresh UI data + await refreshSystemData(); + return matchedCount; + } else { + showNotification(`Import Failed: None of the ${totalFoundInFile} students in this file exist in your system.`, 'error'); + return 0; + } + }; const processCSV = (text: string) => { @@ -694,6 +860,8 @@ const handleFileChange = (e: React.ChangeEvent) => { e.target.value = ''; }; + // Handles the Student Information file upload + // Blocks Attendance files to prevent "ghost" students const handleStudentInfoFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; @@ -701,20 +869,36 @@ const handleFileChange = (e: React.ChangeEvent) => { const reader = new FileReader(); reader.onload = async (event) => { const text = event.target?.result as string; + + // VALIDATION: Check what kind of file this actually is + const actualType = detectFileType(text); + + // SECURITY: If user tries to upload Attendance as Student Info + if (actualType === 'enrollments') { + showNotification("Format Error: You cannot upload an Attendance List as Student Information!", 'error'); + return; + } + + // SECURITY: Ensure it's a student list + if (actualType !== 'students' && actualType !== 'unknown') { + showNotification("Format Error: This file does not contain valid Student Information!", 'error'); + return; + } + try { const rows = text.split('\n').map(r => r.trim()).filter(r => r.length > 0); - // Buffer Mode + // Extract students and store them in the temporary map const map = extractStudentMap(rows); map.forEach((val, key) => tempStudentMap.current.set(key, val)); - const count = map.size; // Showing map.size is count from this file - setStudentInfoStatus({ fileName: file.name, count }); + + setStudentInfoStatus({ fileName: file.name, count: map.size }); + showNotification(`Successfully loaded ${map.size} students. Click 'Done' to save.`, 'success'); } catch (err) { - console.error("Failed to parse student info file", err); - showNotification(t('dataInput.parseError'), 'error'); + showNotification("Parse Error: Failed to process the student info file.", 'error'); } }; reader.readAsText(file); - e.target.value = ''; + e.target.value = ''; // Reset to allow re-selection }; diff --git a/src/components/ScheduleView.tsx b/src/components/ScheduleView.tsx index 8b2b9ab..e049e13 100644 --- a/src/components/ScheduleView.tsx +++ b/src/components/ScheduleView.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { ExamSession, Course, Classroom, Student } from '../types'; +import { ExamSession, Course, Classroom, Student, DisplaySession } from '../types'; import { TimeSlotDetailModal } from './TimeSlotDetailModal'; interface ScheduleViewProps { @@ -21,7 +21,7 @@ export const ScheduleView: React.FC = ({ schedule, courses, c // Time slot detail modal state const [showTimeSlotModal, setShowTimeSlotModal] = useState(false); - const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); + const [selectedTimeSlotExams, setSelectedTimeSlotExams] = useState([]); const [selectedTimeSlot, setSelectedTimeSlot] = useState<{ start: Date; end: Date } | null>(null); // Initialize currentDate based on the first exam in the schedule, or today if schedule is empty @@ -64,6 +64,7 @@ export const ScheduleView: React.FC = ({ schedule, courses, c }); }, [weekStart]); + // 1. Step: Basic Filtering const filteredSessions = useMemo(() => { if (filterMode === 'all') return schedule; @@ -80,11 +81,45 @@ export const ScheduleView: React.FC = ({ schedule, courses, c }); }, [schedule, filterMode, selectedEntityId, students]); - // Group sessions by time slot (sessions with the same start time) - const groupedSessionsByTimeSlot = useMemo(() => { - const groups = new Map(); + // 2. Step: Merge Split Exams (TEK DEĞİŞİKLİK BURADA BAŞLIYOR) + // Aynı ders, aynı saatte ise bunları tek bir görüntüleme objesinde birleştiriyoruz. + const processedSessions = useMemo(() => { + + // Oda modunda birleştirme yapmıyoruz çünkü odaya özel görüntüleme istiyoruz + if (filterMode === 'room') return filteredSessions as DisplaySession[]; + + const groupedMap = new Map(); filteredSessions.forEach(session => { + // TypeScript için casting (studentCount backend'den geliyor olabilir ama tipte yoksa hata vermesin) + const sCount = (session as any).studentCount || 0; + + // Anahtar: Ders Kodu + Başlangıç Zamanı + const key = `${session.courseId}-${new Date(session.startTime).toISOString()}`; + + if (!groupedMap.has(key)) { + groupedMap.set(key, { + ...session, + isSplit: false, + classroomList: [{ name: session.classroomId, count: sCount }], + totalStudents: sCount + }); + } else { + const existing = groupedMap.get(key)!; + existing.isSplit = true; + existing.classroomList?.push({ name: session.classroomId, count: sCount }); + existing.totalStudents = (existing.totalStudents || 0) + sCount; + } + }); + + return Array.from(groupedMap.values()); + }, [filteredSessions, filterMode]); + + // 3. Step: Group by Time Slot (processedSessions üzerinden) + const groupedSessionsByTimeSlot = useMemo(() => { + const groups = new Map(); + + processedSessions.forEach(session => { const startTime = new Date(session.startTime); // Create a key from the date and time const key = startTime.toISOString(); @@ -96,11 +131,13 @@ export const ScheduleView: React.FC = ({ schedule, courses, c }); return groups; - }, [filteredSessions]); + }, [processedSessions]); + // Handle time slot card click - const handleTimeSlotClick = (exams: ExamSession[]) => { + const handleTimeSlotClick = (exams: DisplaySession[]) => { if (exams.length > 0) { + // Modal için veriyi hazırlarken gerekirse split detaylarını açabiliriz ama şimdilik özet geçiyoruz setSelectedTimeSlotExams(exams); setSelectedTimeSlot({ start: new Date(exams[0].startTime), @@ -113,7 +150,7 @@ export const ScheduleView: React.FC = ({ schedule, courses, c const START_HOUR = 8; const END_HOUR = 22; const HOURS_COUNT = END_HOUR - START_HOUR; - const HOUR_HEIGHT = 64; + const HOUR_HEIGHT = 80; // Split badge'leri sığsın diye yüksekliği 64'ten 80'e çıkardım const formatDate = (date: Date) => { return date.toLocaleDateString(i18n.language === 'tr' ? 'tr-TR' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' }); @@ -134,24 +171,53 @@ export const ScheduleView: React.FC = ({ schedule, courses, c setCurrentDate(new Date(e.target.value)); } }; - - const handleExport = async () => { +const handleExport = async () => { setIsExporting(true); setExportMessage(null); try { - const sessionsForExport = schedule.map(s => ({ - sessionId: s.id, - courseCode: s.courseId, - classroomName: s.classroomId, - startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, - endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, - })); + + const sortedSchedule = [...schedule].sort((a, b) => { + + const timeDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + if (timeDiff !== 0) return timeDiff; + + + return a.courseId.localeCompare(b.courseId); + }); + + let examCounter = 0; + let lastKey = ''; + + const sessionsForExport = sortedSchedule.map(s => { + const course = courses.find(c => c.id === s.courseId); + const classroom = classrooms.find(c => c.id === s.classroomId); + + + const currentKey = `${s.courseId}-${new Date(s.startTime).toISOString()}`; + + + if (currentKey !== lastKey) { + examCounter++; + lastKey = currentKey; + } + + return { + + sessionId: `Exam ${examCounter}`, + courseCode: course?.code || s.courseId, + classroomName: classroom?.name || s.classroomId, + startTime: s.startTime instanceof Date ? s.startTime.toISOString() : s.startTime, + endTime: s.endTime instanceof Date ? s.endTime.toISOString() : s.endTime, + + studentCount: (s as any).studentCount || 0 + }; + }); const result = await window.api.exportScheduleCSV({ sessions: sessionsForExport, courses: courses.map(c => ({ id: c.id, code: c.code, name: c.name })), - classrooms: classrooms.map(c => ({ id: c.id, name: c.name })), + classrooms: classrooms.map(c => ({ id: c.id, name: c.name, capacity: c.capacity })), }); if (result.success) { @@ -283,7 +349,7 @@ export const ScheduleView: React.FC = ({ schedule, courses, c
-
+
{/* Increased width for split badges */}
GMT+0 @@ -332,8 +398,13 @@ export const ScheduleView: React.FC = ({ schedule, courses, c const top = (startMinutes / 60) * HOUR_HEIGHT; const height = (durationMinutes / 60) * HOUR_HEIGHT; - const isMultipleExams = examsInSlot.length > 1; + // Condition 1: Multiple Different Courses in same slot + const isMultipleDifferentExams = examsInSlot.length > 1; + + // Condition 2: Single Course but Split (Has merged classroom list) const firstExam = examsInSlot[0]; + const isSplitExam = firstExam.isSplit; + const course = courses.find(c => c.id === firstExam.courseId); const room = classrooms.find(r => r.id === firstExam.classroomId); @@ -344,16 +415,16 @@ export const ScheduleView: React.FC = ({ schedule, courses, c style={{ top: `${top}px`, height: `${height}px`, - backgroundColor: isMultipleExams ? 'rgba(219, 234, 254, 0.98)' : 'rgba(239, 246, 255, 0.95)', - borderColor: isMultipleExams ? '#3b82f6' : '#6366f1', + backgroundColor: isMultipleDifferentExams ? 'rgba(219, 234, 254, 0.98)' : (isSplitExam ? '#fff7ed' : 'rgba(239, 246, 255, 0.95)'), + borderColor: isMultipleDifferentExams ? '#3b82f6' : (isSplitExam ? '#f97316' : '#6366f1'), borderWidth: '1px', borderLeftWidth: '4px' }} onClick={() => handleTimeSlotClick(examsInSlot)} >
- {isMultipleExams ? ( - /* Multiple exams - show count and summary */ + {isMultipleDifferentExams ? ( + /* Multiple different courses at same time */ <>
@@ -383,26 +454,67 @@ export const ScheduleView: React.FC = ({ schedule, courses, c
) : ( - /* Single exam - show details directly */ + /* Single Course (Split or Normal) */ <> +
{course?.code}
-
- {course?.name} -
- {filterMode !== 'room' && ( -
- - {room?.name} -
+ {isSplitExam && ( + + {t('schedule.split', 'Split')} + )} - {filterMode === 'room' && ( -
- - {course?.enrolledStudents} -
+
+
+ {course?.name} +
+ +
+ {isSplitExam ? ( + // --- SPLIT EXAM DISPLAY --- + <> +
+ + {t('schedule.totalStudents', { count: firstExam.totalStudents })} +
+
+ {(firstExam.classroomList || []).slice(0, 2).map((cr, idx) => ( + + {classrooms.find(r => r.id === cr.name)?.name || cr.name} + ({cr.count} / {classrooms.find(r => r.id === cr.name)?.capacity || '?'}) + + ))} + {firstExam.classroomList && firstExam.classroomList.length > 2 && ( + + +{firstExam.classroomList.length - 2} {t('common.more')} + + )} +
+ + ) : ( + // --- NORMAL EXAM DISPLAY --- + <> + {filterMode !== 'room' && ( +
+ + {room?.name} + {firstExam.studentCount && room?.capacity ? ( + ({firstExam.studentCount} / {room.capacity}) + ) : firstExam.studentCount ? ( + ({firstExam.studentCount}) + ) : null} +
+ )} + {filterMode === 'room' && ( +
+ + {course?.enrolledStudents} +
+ )} + )} +
)}
@@ -426,4 +538,4 @@ export const ScheduleView: React.FC = ({ schedule, courses, c />
); -}; +}; \ No newline at end of file diff --git a/src/components/TimeSlotDetailModal.tsx b/src/components/TimeSlotDetailModal.tsx index c99581a..74987a7 100644 --- a/src/components/TimeSlotDetailModal.tsx +++ b/src/components/TimeSlotDetailModal.tsx @@ -1,12 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { useTranslation } from 'react-i18next'; -import { ExamSession, Course, Classroom } from '../types'; +import { DisplaySession, Course, Classroom } from '../types'; // Use DisplaySession interface TimeSlotDetailModalProps { isOpen: boolean; onClose: () => void; - exams: ExamSession[]; + exams: DisplaySession[]; // Changed from ExamSession[] courses: Course[]; classrooms: Classroom[]; timeSlot: { start: Date; end: Date } | null; @@ -40,29 +40,21 @@ export const TimeSlotDetailModal: React.FC = ({ }); }; - // Color palette for different classrooms - const classroomColors = [ - { bg: 'bg-ieu-50', border: 'border-ieu-400', text: 'text-ieu-600' }, - { bg: 'bg-emerald-50', border: 'border-emerald-400', text: 'text-emerald-700' }, - { bg: 'bg-amber-50', border: 'border-amber-400', text: 'text-amber-700' }, - { bg: 'bg-rose-50', border: 'border-rose-400', text: 'text-rose-700' }, - { bg: 'bg-purple-50', border: 'border-purple-400', text: 'text-purple-700' }, - { bg: 'bg-cyan-50', border: 'border-cyan-400', text: 'text-cyan-700' }, - { bg: 'bg-orange-50', border: 'border-orange-400', text: 'text-orange-700' }, - { bg: 'bg-teal-50', border: 'border-teal-400', text: 'text-teal-700' }, - ]; + // This creates a more visually distinct set of colors + const a = [ + { bg: 'bg-ieu-50', border: 'border-ieu-300', text: 'text-ieu-800' }, + { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-800' }, + { bg: 'bg-sky-50', border: 'border-sky-300', text: 'text-sky-800' }, + { bg: 'bg-amber-50', border: 'border-amber-300', text: 'text-amber-800' }, + { bg: 'bg-rose-50', border: 'border-rose-300', text: 'text-rose-800' }, + { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-800' }, + { bg: 'bg-cyan-50', border: 'border-cyan-300', text: 'text-cyan-800' }, + { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-800' }, + ] - // Map classrooms to colors - const classroomColorMap = new Map(); - const uniqueClassrooms = [...new Set(exams.map(e => e.classroomId))]; - uniqueClassrooms.forEach((classroomId, index) => { - classroomColorMap.set(classroomId, classroomColors[index % classroomColors.length]); - }); - - // Use Portal to render modal at document body level return ReactDOM.createPortal(
= ({
-

+

{t('schedule.timeSlotDetails')}

-

+

{formatDate(timeSlot.start)} -

-

+ | {formatTime(timeSlot.start)} - {formatTime(timeSlot.end)}

-
- - {t('schedule.multipleExams', { count: exams.length })} - -
{/* Exam List */}
{exams.map((exam, index) => { const course = courses.find(c => c.id === exam.courseId); - const classroom = classrooms.find(c => c.id === exam.classroomId); - const colorScheme = classroomColorMap.get(exam.classroomId) || classroomColors[0]; - + return (
-
-
-
- - {course?.code || exam.courseId} - - #{index + 1} -
-

- {course?.name || t('common.noData')} -

-
+ {/* Course Info Header */} +
+

+ {course?.code || exam.courseId} +

+

+ {course?.name || t('common.noData')} +

- -
- {/* Classroom */} -
- - - - - - {classroom?.name || exam.classroomId} - - {classroom && ( - - ({t('schedule.capacityShort', { capacity: classroom.capacity })}) - - )} -
- - {/* Enrolled Students */} -
- - - - - - - - {t('schedule.enrolledCount', { count: course?.enrolledStudents || 0 })} - -
+ + {/* Body (SPLIT LOGIC HERE) */} +
+ {exam.isSplit ? ( + // --- RENDER SPLIT EXAM --- +
+
+

+ {t('schedule.splitClassrooms')} +

+ + {t('schedule.enrolledCount', { count: exam.totalStudents || course?.enrolledStudents || 0 })} + +
+
+ {exam.classroomList?.map((cr, index) => { + const classroom = classrooms.find(c => c.id === cr.name); + return ( +
+
+ + {classroom?.name || cr.name} + ({t('schedule.capacityShort', { capacity: classroom?.capacity || 0 })}) +
+ + {t('schedule.studentCount', { count: cr.count })} + +
+ ) + })} +
+
+ ) : ( + // --- RENDER NORMAL EXAM --- +
+
+ + {classrooms.find(c=>c.id === exam.classroomId)?.name || exam.classroomId} +
+
+ + {t('schedule.enrolledCount', { count: exam.studentCount || course?.enrolledStudents || 0 })} +
+
+ )}
); @@ -166,13 +158,13 @@ export const TimeSlotDetailModal: React.FC = ({
, document.body ); -}; +}; \ No newline at end of file diff --git a/src/locales/en.json b/src/locales/en.json index e6120e7..54fd1dc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -18,7 +18,8 @@ "previousWeek": "Previous Week", "nextWeek": "Next Week", "today": "Today", - "done": "Done" + "done": "Done", + "more": "more" }, "dashboard": { "title": "Dashboard", @@ -123,7 +124,11 @@ "multipleExams": "{{count}} Exams", "timeSlotDetails": "Time Slot Details", "clickForDetails": "Click for details", - "enrolledCount": "Enrolled: {{count}}" + "enrolledCount": "Enrolled: {{count}}", + "splitClassrooms": "Split Classrooms", + "split": "Split", + "totalStudents": "Total: {{count}}", + "studentCount": "Students: {{count}}" }, "settings": { "title": "Settings", diff --git a/src/locales/tr.json b/src/locales/tr.json index 540ef62..cce7c26 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -18,7 +18,8 @@ "previousWeek": "Önceki Hafta", "nextWeek": "Sonraki Hafta", "today": "Bugün", - "done": "Tamam" + "done": "Tamam", + "more": "daha fazla" }, "dashboard": { "title": "Panel", @@ -121,7 +122,11 @@ "multipleExams": "{{count}} Sınav", "timeSlotDetails": "Zaman Dilimi Detayları", "clickForDetails": "Detaylar için tıklayın", - "enrolledCount": "Kayıtlı: {{count}}" + "enrolledCount": "Kayıtlı: {{count}}", + "splitClassrooms": "Bölünmüş Derslikler", + "split": "Bölünmüş", + "totalStudents": "Toplam: {{count}}", + "studentCount": "Öğrenci: {{count}}" }, "settings": { "title": "Ayarlar", diff --git a/src/types/index.ts b/src/types/index.ts index d84c489..c470701 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -31,7 +31,7 @@ export interface Classroom { } export interface Student { - id: string; + id:string; name: string; email: string; enrolledCourses: string[]; // Array of Course IDs @@ -43,6 +43,7 @@ export interface ExamSession { classroomId: string; startTime: Date; endTime: Date; + studentCount?: number; } export interface DataContextType { @@ -50,3 +51,13 @@ export interface DataContextType { classrooms: Classroom[]; students: Student[]; } + +/** + * Represents an exam session that has been processed for UI display. + * It may be a single session or a combination of multiple sessions for a split exam. + */ +export interface DisplaySession extends ExamSession { + isSplit?: boolean; + classroomList?: { name: string; count: number }[]; // List of classrooms for a split exam + totalStudents?: number; +} \ No newline at end of file