diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..01d4da2 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,19 @@ +#!/bin/bash + +# Git pre-commit hook to run ESLint +# This hook runs on every commit to ensure code quality + +echo "🔍 Running ESLint check..." + +# Run lint check +npm run lint + +# Check the exit status +if [ $? -ne 0 ]; then + echo "❌ ESLint failed. Please fix the linting errors before committing." + echo "💡 Run 'npm run lint' to see the issues" + exit 1 +fi + +echo "✅ ESLint passed! Commit is proceeding..." +exit 0 \ No newline at end of file diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..a04cd31 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,19 @@ +#!/bin/bash + +# Git pre-push hook to run build check +# This hook runs before pushing to ensure the project builds successfully + +echo "🏗️ Running build check before push..." + +# Run build check +npm run build + +# Check the exit status +if [ $? -ne 0 ]; then + echo "❌ Build failed. Please fix the build errors before pushing." + echo "💡 Run 'npm run build' to see the issues" + exit 1 +fi + +echo "✅ Build passed! Push is proceeding..." +exit 0 \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5021fa6..886ca85 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -52,8 +52,6 @@ jobs: run: npm run lint - name: Build production bundle - env: - NEXT_PUBLIC_API_BASE_URL: https://api.techtorque.randitha.net run: npm run build - name: Run a minimal start for smoke test (background) @@ -106,5 +104,3 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: | - NEXT_PUBLIC_API_BASE_URL=https://api.techtorque.randitha.net diff --git a/Dockerfile b/Dockerfile index c4d73d8..dfca7e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,10 @@ COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static +# Copy entrypoint script +COPY entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + # Change ownership to nextjs user RUN chown -R nextjs:nodejs /app @@ -57,5 +61,5 @@ EXPOSE 3000 ENV HOSTNAME="0.0.0.0" ENV PORT=3000 -# Start the Next.js server -CMD ["node", "server.js"] +# Start via entrypoint script (generates runtime config) +CMD ["/app/entrypoint.sh"] diff --git a/GIT_HOOKS.md b/GIT_HOOKS.md new file mode 100644 index 0000000..1fdc073 --- /dev/null +++ b/GIT_HOOKS.md @@ -0,0 +1,61 @@ +# Git Hooks + +This project uses git hooks to maintain code quality and ensure builds pass before commits and pushes. + +## Hooks Installed + +- **pre-commit**: Runs `npm run lint` before each commit + - Prevents commits if ESLint errors are found + - Ensures code follows the project's linting standards + +- **pre-push**: Runs `npm run build` before each push + - Prevents pushes if the build fails + - Ensures the project compiles successfully before sharing changes + +## Setup + +Run the setup script to configure git hooks: + +```bash +./setup-hooks.sh +``` + +Or manually configure: + +```bash +chmod +x .githooks/pre-commit .githooks/pre-push +git config core.hooksPath .githooks +``` + +## Bypassing Hooks + +Sometimes you may need to bypass hooks (use sparingly): + +```bash +# Skip pre-commit hook +git commit --no-verify -m "your message" + +# Skip pre-push hook +git push --no-verify +``` + +## Hook Details + +### Pre-commit Hook +- Runs automatically on `git commit` +- Executes `npm run lint` +- Exits with error code if linting fails +- Shows helpful error messages + +### Pre-push Hook +- Runs automatically on `git push` +- Executes `npm run build` +- Exits with error code if build fails +- Prevents pushing broken code to remote repository + +## Benefits + +✅ **Code Quality**: Ensures all committed code passes linting standards +✅ **Build Safety**: Prevents pushing code that doesn't compile +✅ **Team Consistency**: All developers follow the same quality checks +✅ **CI/CD Friendly**: Reduces failed builds in CI/CD pipelines \ No newline at end of file diff --git a/README.md b/README.md index a8db4a8..1a202df 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,9 @@ This repository contains the source code for the TechTorque 2025 customer and em 3. **Access Application:** Open [http://localhost:3000](http://localhost:3000) in your browser. + +4. **Setup Git Hooks (Recommended):** + ```bash + npm run setup-hooks + ``` + This configures automatic linting on commit and build checking on push. See [GIT_HOOKS.md](GIT_HOOKS.md) for details. diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..000088b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# entrypoint.sh - Generate runtime config and start Next.js + +# Generate runtime configuration file +cat > /app/public/runtime-config.js <=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -178,9 +184,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -191,9 +197,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -201,13 +207,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.17.0", "levn": "^0.4.1" }, "engines": { @@ -694,19 +700,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.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", @@ -970,12 +963,18 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", - "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.14.1.tgz", + "integrity": "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ==", "dev": true, "license": "MIT" }, + "node_modules/@stomp/stompjs": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.2.1.tgz", + "integrity": "sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -986,54 +985,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", - "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz", + "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", - "jiti": "^2.5.1", - "lightningcss": "1.30.1", - "magic-string": "^0.30.18", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.19", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.16" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", - "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz", + "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.4", - "tar": "^7.4.3" - }, "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-arm64": "4.1.13", - "@tailwindcss/oxide-darwin-x64": "4.1.13", - "@tailwindcss/oxide-freebsd-x64": "4.1.13", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", - "@tailwindcss/oxide-linux-x64-musl": "4.1.13", - "@tailwindcss/oxide-wasm32-wasi": "4.1.13", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + "@tailwindcss/oxide-android-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-arm64": "4.1.16", + "@tailwindcss/oxide-darwin-x64": "4.1.16", + "@tailwindcss/oxide-freebsd-x64": "4.1.16", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", + "@tailwindcss/oxide-linux-x64-musl": "4.1.16", + "@tailwindcss/oxide-wasm32-wasi": "4.1.16", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", - "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz", + "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==", "cpu": [ "arm64" ], @@ -1048,9 +1042,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", - "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz", + "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==", "cpu": [ "arm64" ], @@ -1065,9 +1059,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", - "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz", + "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==", "cpu": [ "x64" ], @@ -1082,9 +1076,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", - "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz", + "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==", "cpu": [ "x64" ], @@ -1099,9 +1093,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", - "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz", + "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==", "cpu": [ "arm" ], @@ -1116,9 +1110,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", - "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz", + "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==", "cpu": [ "arm64" ], @@ -1133,9 +1127,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", - "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz", + "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==", "cpu": [ "arm64" ], @@ -1150,9 +1144,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", - "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz", + "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==", "cpu": [ "x64" ], @@ -1167,9 +1161,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", - "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz", + "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==", "cpu": [ "x64" ], @@ -1184,9 +1178,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", - "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz", + "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1202,21 +1196,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.5", - "@emnapi/runtime": "^1.4.5", - "@emnapi/wasi-threads": "^1.0.4", - "@napi-rs/wasm-runtime": "^0.2.12", - "@tybys/wasm-util": "^0.10.0", - "tslib": "^2.8.0" + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.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.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", - "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz", + "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==", "cpu": [ "arm64" ], @@ -1231,9 +1225,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", - "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz", + "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==", "cpu": [ "x64" ], @@ -1248,17 +1242,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.13.tgz", - "integrity": "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz", + "integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.13", - "@tailwindcss/oxide": "4.1.13", + "@tailwindcss/node": "4.1.16", + "@tailwindcss/oxide": "4.1.16", "postcss": "^8.4.41", - "tailwindcss": "4.1.13" + "tailwindcss": "4.1.16" } }, "node_modules/@tybys/wasm-util": { @@ -1301,9 +1295,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", - "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", "dev": true, "license": "MIT", "dependencies": { @@ -1311,37 +1305,45 @@ } }, "node_modules/@types/react": { - "version": "19.1.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", - "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, + "node_modules/@types/sockjs-client": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz", + "integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz", - "integrity": "sha512-molgphGqOBT7t4YKCSkbasmu1tb1MgrZ2szGzHbclF7PNmOkSTQVHy+2jXOSnxvR3+Xe1yySHFZoqMpz3TfQsw==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/type-utils": "8.44.1", - "@typescript-eslint/utils": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1355,7 +1357,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.44.1", + "@typescript-eslint/parser": "^8.46.3", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1371,16 +1373,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.1.tgz", - "integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4" }, "engines": { @@ -1396,14 +1399,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.1.tgz", - "integrity": "sha512-ycSa60eGg8GWAkVsKV4E6Nz33h+HjTXbsDT4FILyL8Obk5/mx4tbvCNsLf9zret3ipSumAOG89UcCs/KRaKYrA==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.44.1", - "@typescript-eslint/types": "^8.44.1", + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", "debug": "^4.3.4" }, "engines": { @@ -1418,14 +1421,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.1.tgz", - "integrity": "sha512-NdhWHgmynpSvyhchGLXh+w12OMT308Gm25JoRIyTZqEbApiBiQHD/8xgb6LqCWCFcxFtWwaVdFsLPQI3jvhywg==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1" + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1436,9 +1439,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.1.tgz", - "integrity": "sha512-B5OyACouEjuIvof3o86lRMvyDsFwZm+4fBOqFHccIctYgBjqR3qT39FBYGN87khcgf0ExpdCBeGKpKRhSFTjKQ==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", "dev": true, "license": "MIT", "engines": { @@ -1453,15 +1456,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.1.tgz", - "integrity": "sha512-KdEerZqHWXsRNKjF9NYswNISnFzXfXNDfPxoTh7tqohU/PRIbwTmsjGK6V9/RTYWau7NZvfo52lgVk+sJh0K3g==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1", - "@typescript-eslint/utils": "8.44.1", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1478,9 +1481,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.1.tgz", - "integrity": "sha512-Lk7uj7y9uQUOEguiDIDLYLJOrYHQa7oBiURYVFqIpGxclAFQ78f6VUOM8lI2XEuNOKNB7XuvM2+2cMXAoq4ALQ==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", "dev": true, "license": "MIT", "engines": { @@ -1492,16 +1495,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.1.tgz", - "integrity": "sha512-qnQJ+mVa7szevdEyvfItbO5Vo+GfZ4/GZWWDRRLjrxYPkhM+6zYB2vRYwCsoJLzqFCdZT4mEqyJoyzkunsZ96A==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.44.1", - "@typescript-eslint/tsconfig-utils": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/visitor-keys": "8.44.1", + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1577,16 +1580,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.1.tgz", - "integrity": "sha512-DpX5Fp6edTlocMCwA+mHY8Mra+pPjRZ0TfHkXI8QFelIKcbADQz1LUPNtzOFUriBB2UYqw4Pi9+xV4w9ZczHFg==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.44.1", - "@typescript-eslint/types": "8.44.1", - "@typescript-eslint/typescript-estree": "8.44.1" + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1601,13 +1604,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.44.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.1.tgz", - "integrity": "sha512-576+u0QD+Jp3tZzvfRfxon0EA2lzcDt3lhUbsC6Lgzy9x2VR4E+JUiNyGHi5T8vk0TV+fpJ5GLG1JsJuWCaKhw==", + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.44.1", + "@typescript-eslint/types": "8.46.3", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1893,6 +1896,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2160,9 +2164,9 @@ } }, "node_modules/axe-core": { - "version": "4.10.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", - "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", + "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", "dev": true, "license": "MPL-2.0", "engines": { @@ -2170,9 +2174,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2281,9 +2285,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "funding": [ { "type": "opencollective", @@ -2317,16 +2321,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2526,9 +2520,9 @@ } }, "node_modules/detect-libc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", - "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "devOptional": true, "license": "Apache-2.0", "engines": { @@ -2770,25 +2764,25 @@ } }, "node_modules/eslint": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", - "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.36.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -2949,6 +2943,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3195,6 +3190,15 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3256,6 +3260,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3412,6 +3428,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -3468,9 +3494,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3640,6 +3666,12 @@ "node": ">= 0.4" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3677,6 +3709,12 @@ "node": ">=0.8.19" } }, + "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/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -3864,14 +3902,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "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.3", - "get-proto": "^1.0.0", + "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" }, @@ -4126,9 +4165,9 @@ } }, "node_modules/jiti": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", - "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "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": { @@ -4259,9 +4298,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", - "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "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": { @@ -4275,22 +4314,44 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.30.1", - "lightningcss-darwin-x64": "1.30.1", - "lightningcss-freebsd-x64": "1.30.1", - "lightningcss-linux-arm-gnueabihf": "1.30.1", - "lightningcss-linux-arm64-gnu": "1.30.1", - "lightningcss-linux-arm64-musl": "1.30.1", - "lightningcss-linux-x64-gnu": "1.30.1", - "lightningcss-linux-x64-musl": "1.30.1", - "lightningcss-win32-arm64-msvc": "1.30.1", - "lightningcss-win32-x64-msvc": "1.30.1" + "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.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", - "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "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" ], @@ -4309,9 +4370,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", - "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "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" ], @@ -4330,9 +4391,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", - "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", "cpu": [ "x64" ], @@ -4351,9 +4412,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", - "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "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" ], @@ -4372,9 +4433,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", - "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "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" ], @@ -4393,9 +4454,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", - "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "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" ], @@ -4414,9 +4475,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", - "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "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" ], @@ -4435,9 +4496,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", - "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "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" ], @@ -4456,9 +4517,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", - "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "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" ], @@ -4477,9 +4538,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", - "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "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" ], @@ -4534,9 +4595,9 @@ } }, "node_modules/magic-string": { - "version": "0.30.19", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "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": { @@ -4620,34 +4681,10 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "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/nanoid": { @@ -4669,9 +4706,9 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", - "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "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": { @@ -5098,6 +5135,12 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5124,6 +5167,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5133,6 +5177,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5191,14 +5236,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "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.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5287,6 +5338,26 @@ "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", @@ -5329,9 +5400,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "devOptional": true, "license": "ISC", "bin": { @@ -5532,6 +5603,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5748,16 +5847,16 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", - "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz", + "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==", "dev": true, "license": "MIT" }, "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "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": { @@ -5768,23 +5867,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5826,6 +5908,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5970,11 +6053,12 @@ } }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6054,6 +6138,39 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6169,16 +6286,6 @@ "node": ">=0.10.0" } }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 64bf670..f80d5b9 100644 --- a/package.json +++ b/package.json @@ -6,14 +6,17 @@ "dev": "next dev --turbopack", "build": "next build --turbopack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "setup-hooks": "chmod +x .githooks/pre-commit .githooks/pre-push && git config core.hooksPath .githooks && echo '✅ Git hooks configured successfully!'" }, "dependencies": { + "@stomp/stompjs": "^7.2.1", "axios": "^1.12.2", "js-cookie": "^3.0.5", "next": "^16.0.1", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "sockjs-client": "^1.6.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -22,6 +25,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/sockjs-client": "^1.5.4", "eslint": "^9", "eslint-config-next": "15.5.3", "tailwindcss": "^4", diff --git a/public/runtime-config.js b/public/runtime-config.js new file mode 100644 index 0000000..8a09c7d --- /dev/null +++ b/public/runtime-config.js @@ -0,0 +1,13 @@ +// runtime-config.js +// This file is intentionally simple and is loaded before the app hydrates. +// In production you may overwrite this file at container startup with real +// environment values. Example generation (Linux entrypoint): +// cat > /usr/share/app/public/runtime-config.js <(null); const [warning, setWarning] = useState(null); + const [unverifiedEmail, setUnverifiedEmail] = useState(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setWarning(null); + setUnverifiedEmail(null); setLoading(true); const form = e.currentTarget; const formData = new FormData(form); + const email = (formData.get('username') as string) || ''; const payload: LoginRequest = { - username: (formData.get('username') as string) || '', + username: email, password: (formData.get('password') as string) || '', }; try { @@ -51,7 +54,14 @@ export default function LoginPage() { } } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : 'Login failed'; - setError(errorMessage); + + // Check if error is about email verification + if (errorMessage.includes('verify your email') || errorMessage.includes('email address')) { + setUnverifiedEmail(email); + setError(errorMessage); + } else { + setError(errorMessage); + } } finally { setLoading(false); } @@ -149,13 +159,21 @@ export default function LoginPage() { {error && ( -
- {error} +
+

{error}

+ {unverifiedEmail && ( + + Resend Verification Email → + + )}
)} {warning && ( -
- {warning} +
+

{warning}

)}
diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx index 05d4d20..1dafed5 100644 --- a/src/app/auth/register/page.tsx +++ b/src/app/auth/register/page.tsx @@ -2,7 +2,6 @@ import React from "react"; import Link from "next/link"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import authService from "../../../services/authService"; import ThemeToggle from "../../components/ThemeToggle"; @@ -70,8 +69,6 @@ const EyeOffIcon = () => ( // --- Register Page Component --- export default function RegisterPage() { - const router = useRouter(); - const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); @@ -118,10 +115,7 @@ export default function RegisterPage() { try { await authService.register(payload); setSuccess(true); - // Redirect to login after 3 seconds - setTimeout(() => { - router.push("/auth/login"); - }, 3000); + // Don't automatically redirect - let user see the success message } catch (err: unknown) { const errorMessage = err instanceof Error ? err.message : "Registration failed"; @@ -186,13 +180,31 @@ export default function RegisterPage() {

Welcome to TechTorque Auto, {formData.fullName}!

-

- A verification email has been sent to {formData.email}. Please - verify your email to log in. -

-

- Redirecting to login... -

+
+

+ 📧 Verify Your Email +

+

+ A verification email has been sent to {formData.email}. +
+ You must verify your email before you can log in. +

+

+ Didn't receive the email?{' '} + + Resend verification email + +

+
+ + Go to Login +
) : ( <> diff --git a/src/app/auth/resend-verification/page.tsx b/src/app/auth/resend-verification/page.tsx new file mode 100644 index 0000000..683ced8 --- /dev/null +++ b/src/app/auth/resend-verification/page.tsx @@ -0,0 +1,180 @@ +'use client' + +import React, { useEffect, useState, Suspense } from 'react'; +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; +import authService from '../../../services/authService'; +import ThemeToggle from '../../components/ThemeToggle'; + +// Icon Components +const Icon = ({ d, size = 10 }: { d: string; size?: number }) => ( + + + +); +const BoltIcon = ({size = 10}) => ; +const MailIcon = () => ; + +function ResendVerificationContent() { + const searchParams = useSearchParams(); + const emailParam = searchParams.get('email'); + + const [loading, setLoading] = useState(false); + const [success, setSuccess] = useState(false); + const [error, setError] = useState(null); + const [email, setEmail] = useState(emailParam || ''); + + useEffect(() => { + if (emailParam) { + setEmail(emailParam); + } + }, [emailParam]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setSuccess(false); + setLoading(true); + + try { + await authService.resendVerificationEmail(email); + setSuccess(true); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Failed to resend verification email'; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + return ( + <> + {success ? ( +
+
+ + + +
+

+ Email Sent! +

+

+ We've sent a new verification link to {email}. + Please check your inbox and click the link to verify your account. +

+
+ + Back to Login + + +
+
+ ) : ( +
+
+ + setEmail(e.target.value)} + className="theme-input w-full" + placeholder="you@example.com" + /> +
+ +
+ +
+
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!success && ( +
+

+ Remember your password?{' '} + + Sign In + +

+
+ )} + + ); +} + +export default function ResendVerificationPage() { + return ( +
+ {/* Header */} +
+
+
+ +
+ +
+

+ TechTorque Auto +

+ +
+ + Back to Login + + +
+
+
+
+ + {/* Main Content */} +
+
+
+
+
+

+ Resend Verification Email +

+

+ Enter your email address to receive a new verification link. +

+
+ + Loading...
}> + + +
+
+ + + ); +} diff --git a/src/app/components/AddVehicleForm.tsx b/src/app/components/AddVehicleForm.tsx index f124ca8..c5a9c0d 100644 --- a/src/app/components/AddVehicleForm.tsx +++ b/src/app/components/AddVehicleForm.tsx @@ -30,7 +30,17 @@ export default function AddVehicleForm({ onSuccess, onCancel }: AddVehicleFormPr await vehicleService.registerVehicle(formData); onSuccess(); } catch (err: unknown) { - const errorMessage = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to add vehicle'; + let errorMessage = 'Failed to add vehicle'; + + const errorResponse = (err as { response?: { status?: number; data?: { message?: string } } }); + + // Handle 409 Conflict - Duplicate VIN + if (errorResponse?.response?.status === 409) { + errorMessage = 'This VIN is already registered in the system. Please check the VIN and try again.'; + } else { + errorMessage = errorResponse?.response?.data?.message || errorMessage; + } + setError(errorMessage); } finally { setLoading(false); diff --git a/src/app/components/EditVehicleForm.tsx b/src/app/components/EditVehicleForm.tsx index 28b1671..48f10a1 100644 --- a/src/app/components/EditVehicleForm.tsx +++ b/src/app/components/EditVehicleForm.tsx @@ -54,7 +54,9 @@ export default function EditVehicleForm({ return (
-

Update Vehicle

+

+ Update Vehicle - {initialData.licensePlate} +

{error && (
@@ -95,8 +97,10 @@ export default function EditVehicleForm({ name="licensePlate" value={formData.licensePlate} onChange={handleChange} - className="w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 transition-all theme-bg-primary theme-text-primary automotive-border" + className="w-full px-4 py-3 border rounded-xl focus:ring-2 focus:ring-blue-500 transition-all theme-bg-primary theme-text-primary automotive-border opacity-60 cursor-not-allowed" placeholder="e.g., ABC-1234" + disabled + readOnly />
diff --git a/src/app/components/NotificationBell.tsx b/src/app/components/NotificationBell.tsx index 8acd946..39a3407 100644 --- a/src/app/components/NotificationBell.tsx +++ b/src/app/components/NotificationBell.tsx @@ -1,21 +1,14 @@ 'use client' import Link from 'next/link' import { useEffect, useRef, useState } from 'react' -import notificationService from '@/services/notificationService' -import type { NotificationResponse } from '@/types/notification' +import { useNotifications } from '@/app/contexts/NotificationContext' export default function NotificationBell() { - const [unreadCount, setUnreadCount] = useState(0) - const [notifications, setNotifications] = useState([]) + const { notifications, unreadCount, loading, markAsRead, markAllAsRead, fetchNotifications, isConnected } = useNotifications() const [open, setOpen] = useState(false) - const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const panelRef = useRef(null) - useEffect(() => { - refreshUnreadCount() - }, []) - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (panelRef.current && !panelRef.current.contains(event.target as Node)) { @@ -32,43 +25,18 @@ export default function NotificationBell() { return () => document.removeEventListener('mousedown', handleClickOutside) }, [open]) - const refreshUnreadCount = async () => { - try { - const count = await notificationService.getUnreadCount() - setUnreadCount(count) - } catch (err: unknown) { - console.error('Failed to load unread notifications', err) - } - } - - const loadNotifications = async () => { - try { - setLoading(true) - const data = await notificationService.getNotifications() - setNotifications(data) - setError(null) - } catch (err: unknown) { - const message = (err as { response?: { data?: { message?: string } } }).response?.data?.message || 'Failed to load notifications' - setError(message) - } finally { - setLoading(false) - } - } - const handleToggle = async () => { const nextState = !open setOpen(nextState) if (nextState) { - await loadNotifications() - await refreshUnreadCount() + await fetchNotifications() } } const handleMarkAsRead = async (notificationId: string) => { try { - await notificationService.markAsRead(notificationId, true) - await loadNotifications() - await refreshUnreadCount() + await markAsRead(notificationId) + setError(null) } catch (err: unknown) { const message = (err as { response?: { data?: { message?: string } } }).response?.data?.message || 'Failed to mark notification as read' setError(message) @@ -77,9 +45,8 @@ export default function NotificationBell() { const handleMarkAllAsRead = async () => { try { - await notificationService.markAllAsRead() - await loadNotifications() - await refreshUnreadCount() + await markAllAsRead() + setError(null) } catch (err: unknown) { const message = (err as { response?: { data?: { message?: string } } }).response?.data?.message || 'Failed to mark notifications as read' setError(message) @@ -108,21 +75,24 @@ export default function NotificationBell() { /> {unreadCount > 0 && ( - + {unreadCount} )} + {isConnected && ( + + )} {open && ( -
-
- Notifications +
+
+ Notifications {notifications.length > 0 && ( @@ -130,23 +100,23 @@ export default function NotificationBell() {
{loading ? ( -
Loading notifications...
+
Loading notifications...
) : error ? ( -
{error}
+
{error}
) : notifications.length === 0 ? ( -
No notifications yet.
+
No notifications yet.
) : ( -
    +
      {notifications.map((notification) => (
    • -
      -
      -

      {notification.message}

      +
      +
      +

      {notification.message}

      {notification.details && ( -

      {notification.details}

      +

      {notification.details}

      )}

      {new Date(notification.createdAt).toLocaleString()} @@ -156,7 +126,7 @@ export default function NotificationBell() { @@ -167,8 +137,8 @@ export default function NotificationBell() {

    )} View all notifications diff --git a/src/app/components/RoleSwitcher.tsx b/src/app/components/RoleSwitcher.tsx new file mode 100644 index 0000000..8d8f319 --- /dev/null +++ b/src/app/components/RoleSwitcher.tsx @@ -0,0 +1,163 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; + +interface RoleSwitcherProps { + roles: string[]; + currentRole: string; + onRoleChange: (role: string) => void; +} + +const roleConfig = { + SUPER_ADMIN: { + label: 'Super Admin', + icon: '👑', + color: 'text-purple-600 dark:text-purple-400', + bgColor: 'bg-purple-100 dark:bg-purple-900/30', + dashboardPath: '/dashboard', + }, + ADMIN: { + label: 'Admin', + icon: '⚙️', + color: 'text-blue-600 dark:text-blue-400', + bgColor: 'bg-blue-100 dark:bg-blue-900/30', + dashboardPath: '/dashboard', + }, + EMPLOYEE: { + label: 'Employee', + icon: '👷', + color: 'text-green-600 dark:text-green-400', + bgColor: 'bg-green-100 dark:bg-green-900/30', + dashboardPath: '/dashboard', + }, + CUSTOMER: { + label: 'Customer', + icon: '👤', + color: 'text-gray-600 dark:text-gray-400', + bgColor: 'bg-gray-100 dark:bg-gray-800', + dashboardPath: '/dashboard', + }, +}; + +export default function RoleSwitcher({ roles, currentRole, onRoleChange }: RoleSwitcherProps) { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + const router = useRouter(); + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + if (isOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [isOpen]); + + // Only show switcher if user has multiple roles + if (!roles || roles.length <= 1) { + return null; + } + + // Sort roles by priority + const sortedRoles = [...roles].sort((a, b) => { + const priority = { SUPER_ADMIN: 0, ADMIN: 1, EMPLOYEE: 2, CUSTOMER: 3 }; + return (priority[a as keyof typeof priority] ?? 99) - (priority[b as keyof typeof priority] ?? 99); + }); + + const currentConfig = roleConfig[currentRole as keyof typeof roleConfig] || { + label: currentRole, + icon: '🔹', + color: 'text-gray-600', + bgColor: 'bg-gray-100', + dashboardPath: '/dashboard', + }; + + const handleRoleSwitch = (role: string) => { + onRoleChange(role); + setIsOpen(false); + // Optionally refresh the page to update the dashboard view + router.refresh(); + }; + + return ( +
    + {/* Current Role Button */} + + + {/* Dropdown Menu */} + {isOpen && ( +
    +
    +
    + Switch View +
    + {sortedRoles.map((role) => { + const config = roleConfig[role as keyof typeof roleConfig] || { + label: role, + icon: '🔹', + color: 'text-gray-600', + bgColor: 'bg-gray-100', + }; + const isActive = role === currentRole; + + return ( + + ); + })} +
    +
    +

    + 💡 Your current view affects the dashboard and available menu items +

    +
    +
    + )} +
    + ); +} diff --git a/src/app/components/dashboards/CustomerDashboard.tsx b/src/app/components/dashboards/CustomerDashboard.tsx index 0930f5a..fab65b5 100644 --- a/src/app/components/dashboards/CustomerDashboard.tsx +++ b/src/app/components/dashboards/CustomerDashboard.tsx @@ -8,6 +8,7 @@ import type { AppointmentResponseDto } from "@/types/appointment"; import type { UserDto } from "@/types/api"; import type { InvoiceResponseDto } from "@/types/payment"; import type { VehicleListItem } from "@/types/vehicle"; +import AIChatWidget from "@/app/components/chatbot/AIChatWidget"; interface CustomerDashboardProps { profile: UserDto; @@ -19,6 +20,7 @@ const CustomerDashboard: React.FC = ({ profile }) => { const [invoices, setInvoices] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [chatOpen, setChatOpen] = useState(false); useEffect(() => { const loadDashboard = async () => { @@ -218,6 +220,40 @@ const CustomerDashboard: React.FC = ({ profile }) => {
+ + {/* Floating AI Chat Widget */} +
+ {chatOpen ? ( +
+
+

TechTorque AI Assistant

+ +
+
+ +
+
+ ) : ( + + )} +
); }; diff --git a/src/app/components/dashboards/EmployeeDashboard.tsx b/src/app/components/dashboards/EmployeeDashboard.tsx index aafd3ea..4013d55 100644 --- a/src/app/components/dashboards/EmployeeDashboard.tsx +++ b/src/app/components/dashboards/EmployeeDashboard.tsx @@ -39,8 +39,8 @@ const EmployeeDashboard: React.FC = ({ profile }) => { setSchedule(scheduleData) // Ensure timeLogData is an array and filter out invalid entries - const validTimeLogs = Array.isArray(timeLogData) - ? timeLogData.filter(log => log && log.logId) + const validTimeLogs = Array.isArray(timeLogData) + ? timeLogData.filter(log => log && log.id) : [] setTimeLogs(validTimeLogs) setError(null) @@ -61,13 +61,13 @@ const EmployeeDashboard: React.FC = ({ profile }) => { ), [schedule]) const totalHoursThisWeek = useMemo(() => ( - timeLogs.reduce((sum, log) => sum + (Number(log.hoursWorked) || 0), 0) + timeLogs.reduce((sum, log) => sum + (Number(log.hours) || 0), 0) ), [timeLogs]) const workSummaryByDay = useMemo(() => { const summary: Record = {} timeLogs.forEach((log) => { - summary[log.workDate] = (summary[log.workDate] || 0) + (Number(log.hoursWorked) || 0) + summary[log.date] = (summary[log.date] || 0) + (Number(log.hours) || 0) }) return summary }, [timeLogs]) @@ -201,9 +201,9 @@ const EmployeeDashboard: React.FC = ({ profile }) => {

You haven't recorded any time logs yet.

) : ( timeLogs.slice(-5).reverse().map((log, index) => ( -
+

- {new Date(log.workDate).toLocaleDateString()} • {(Number(log.hoursWorked) || 0).toFixed(1)} hrs + {new Date(log.date).toLocaleDateString()} • {(Number(log.hours) || 0).toFixed(1)} hrs

{log.description &&

{log.description}

}
diff --git a/src/app/contexts/DashboardContext.tsx b/src/app/contexts/DashboardContext.tsx index 4a4c2a0..0e69972 100644 --- a/src/app/contexts/DashboardContext.tsx +++ b/src/app/contexts/DashboardContext.tsx @@ -7,6 +7,8 @@ interface DashboardContextState { profile: UserDto | null loading: boolean roles: string[] + activeRole: string + setActiveRole: (role: string) => void refreshProfile: () => Promise } @@ -15,6 +17,7 @@ const DashboardContext = createContext(undefi export function DashboardProvider({ children }: { children: ReactNode }) { const [profile, setProfile] = useState(null) const [loading, setLoading] = useState(true) + const [activeRole, setActiveRole] = useState('') const loadProfile = async () => { try { @@ -22,6 +25,15 @@ export function DashboardProvider({ children }: { children: ReactNode }) { const response = await userService.getCurrentProfile() const payload = (response.data?.data as UserDto | undefined) ?? (response.data as UserDto | null) setProfile(payload ?? null) + + // Set initial active role to the highest privilege role + if (payload?.roles && payload.roles.length > 0) { + const sortedRoles = [...payload.roles].sort((a, b) => { + const priority = { SUPER_ADMIN: 0, ADMIN: 1, EMPLOYEE: 2, CUSTOMER: 3 }; + return (priority[a as keyof typeof priority] ?? 99) - (priority[b as keyof typeof priority] ?? 99); + }); + setActiveRole(sortedRoles[0]); + } } catch (error) { console.error('Failed to load dashboard profile', error) setProfile(null) @@ -38,8 +50,10 @@ export function DashboardProvider({ children }: { children: ReactNode }) { profile, loading, roles: profile?.roles ?? [], + activeRole, + setActiveRole, refreshProfile: loadProfile, - }), [profile, loading]) + }), [profile, loading, activeRole]) return ( diff --git a/src/app/contexts/NotificationContext.tsx b/src/app/contexts/NotificationContext.tsx new file mode 100644 index 0000000..7e74c19 --- /dev/null +++ b/src/app/contexts/NotificationContext.tsx @@ -0,0 +1,202 @@ +"use client"; +import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react'; +import websocketService from '@/services/websocketService'; +import notificationService from '@/services/notificationService'; +import type { NotificationResponse } from '@/types/notification'; + +interface NotificationContextType { + notifications: NotificationResponse[]; + unreadCount: number; + loading: boolean; + isConnected: boolean; + fetchNotifications: (unreadOnly?: boolean) => Promise; + markAsRead: (notificationId: string) => Promise; + markAllAsRead: () => Promise; + deleteNotification: (notificationId: string) => Promise; + showToast: (notification: NotificationResponse) => void; +} + +const NotificationContext = createContext(undefined); + +export const useNotifications = () => { + const context = useContext(NotificationContext); + if (!context) { + throw new Error('useNotifications must be used within NotificationProvider'); + } + return context; +}; + +interface NotificationProviderProps { + children: ReactNode; + userId: string | null; +} + +export const NotificationProvider: React.FC = ({ children, userId }) => { + const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); + const [loading, setLoading] = useState(true); + const [isConnected, setIsConnected] = useState(false); + const [toastNotification, setToastNotification] = useState(null); + + // Fetch notifications from REST API + const fetchNotifications = useCallback(async (unreadOnly = false) => { + if (!userId) return; + + try { + setLoading(true); + const data = await notificationService.getNotifications(unreadOnly); + setNotifications(data); + + // Also fetch unread count + const count = await notificationService.getUnreadCount(); + setUnreadCount(count); + } catch (error) { + console.error('Failed to fetch notifications:', error); + } finally { + setLoading(false); + } + }, [userId]); + + // Mark as read + const markAsRead = useCallback(async (notificationId: string) => { + try { + await notificationService.markAsRead(notificationId, true); + + // Update local state + setNotifications(prev => + prev.map(n => n.notificationId === notificationId ? { ...n, read: true } : n) + ); + setUnreadCount(prev => Math.max(0, prev - 1)); + } catch (error) { + console.error('Failed to mark notification as read:', error); + } + }, []); + + // Mark all as read + const markAllAsRead = useCallback(async () => { + try { + await notificationService.markAllAsRead(); + + // Update local state + setNotifications(prev => prev.map(n => ({ ...n, read: true }))); + setUnreadCount(0); + } catch (error) { + console.error('Failed to mark all notifications as read:', error); + } + }, []); + + // Delete notification + const deleteNotification = useCallback(async (notificationId: string) => { + try { + await notificationService.deleteNotification(notificationId); + + // Update local state + const deletedNotification = notifications.find(n => n.notificationId === notificationId); + setNotifications(prev => prev.filter(n => n.notificationId !== notificationId)); + + if (deletedNotification && !deletedNotification.read) { + setUnreadCount(prev => Math.max(0, prev - 1)); + } + } catch (error) { + console.error('Failed to delete notification:', error); + } + }, [notifications]); + + // Show toast notification + const showToast = useCallback((notification: NotificationResponse) => { + setToastNotification(notification); + + // Auto-hide toast after 5 seconds + setTimeout(() => { + setToastNotification(null); + }, 5000); + }, []); + + // WebSocket callbacks + const handleNewNotification = useCallback((notification: NotificationResponse) => { + console.log('New notification received:', notification); + + // Add to notifications list + setNotifications(prev => [notification, ...prev]); + + // Show toast + showToast(notification); + }, [showToast]); + + const handleUnreadCountUpdate = useCallback((count: number) => { + console.log('Unread count updated:', count); + setUnreadCount(count); + }, []); + + // Initialize WebSocket connection when userId changes + useEffect(() => { + if (!userId) { + websocketService.disconnect(); + setIsConnected(false); + return; + } + + // Fetch initial notifications + fetchNotifications(); + + // Connect to WebSocket + websocketService.connect(userId, handleNewNotification, handleUnreadCountUpdate); + setIsConnected(true); + + // Cleanup on unmount + return () => { + websocketService.disconnect(); + setIsConnected(false); + }; + }, [userId, fetchNotifications, handleNewNotification, handleUnreadCountUpdate]); + + const value: NotificationContextType = { + notifications, + unreadCount, + loading, + isConnected, + fetchNotifications, + markAsRead, + markAllAsRead, + deleteNotification, + showToast, + }; + + return ( + + {children} + {toastNotification && ( +
+
+
+
+

+ {toastNotification.message} +

+ {toastNotification.details && ( +

+ {toastNotification.details} +

+ )} +
+ +
+
+
+ )} +
+ ); +}; + +export default NotificationContext; diff --git a/src/app/dashboard/admin/audit-logs/page.tsx b/src/app/dashboard/admin/audit-logs/page.tsx index a0359a7..699f8f9 100644 --- a/src/app/dashboard/admin/audit-logs/page.tsx +++ b/src/app/dashboard/admin/audit-logs/page.tsx @@ -3,10 +3,13 @@ import { useState, useEffect } from 'react'; import { adminService } from '@/services/adminService'; import { AuditLogResponse } from '@/types/admin'; +import { useDashboard } from '@/app/contexts/DashboardContext'; export default function AuditLogsPage() { + const { roles, loading: rolesLoading } = useDashboard(); const [logs, setLogs] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { loadLogs(); @@ -15,11 +18,15 @@ export default function AuditLogsPage() { const loadLogs = async () => { try { setLoading(true); + setError(null); const data = await adminService.getAuditLogs(); // Ensure data is an array setLogs(Array.isArray(data) ? data : []); } catch (err) { console.error('Failed to load audit logs:', err); + const errorMessage = (err as { response?: { data?: { message?: string } } })?.response?.data?.message || + 'Failed to load audit logs. The service may be unavailable.'; + setError(errorMessage); setLogs([]); // Set empty array on error } finally { setLoading(false); @@ -36,7 +43,11 @@ export default function AuditLogsPage() { }); }; - if (loading) { + // Check if user has permission to access this page + const hasRole = (role: string) => roles?.includes(role); + const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); + + if (rolesLoading || loading) { return (
@@ -48,6 +59,28 @@ export default function AuditLogsPage() { ); } + // Block access for customers and employees + if (!hasAccess) { + return ( +
+
+ + + +

Access Denied

+

+ This page is only accessible to admins and super admins. +

+
+
+ ); + } + return (
@@ -55,6 +88,12 @@ export default function AuditLogsPage() {

System activity and audit trail

+ {error && ( +
+ {error} +
+ )} +
@@ -70,8 +109,16 @@ export default function AuditLogsPage() { {logs.length === 0 ? ( - ) : ( diff --git a/src/app/dashboard/admin/config/page.tsx b/src/app/dashboard/admin/config/page.tsx index a950951..d2116ea 100644 --- a/src/app/dashboard/admin/config/page.tsx +++ b/src/app/dashboard/admin/config/page.tsx @@ -3,8 +3,10 @@ import { useState, useEffect } from 'react'; import { adminService } from '@/services/adminService'; import { SystemConfigurationResponse } from '@/types/admin'; +import { useDashboard } from '@/app/contexts/DashboardContext'; export default function ConfigPage() { + const { roles, loading: rolesLoading } = useDashboard(); const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); @@ -43,7 +45,11 @@ export default function ConfigPage() { return acc; }, {} as Record); - if (loading) { + // Check if user has permission to access this page + const hasRole = (role: string) => roles?.includes(role); + const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); + + if (rolesLoading || loading) { return (
@@ -55,6 +61,28 @@ export default function ConfigPage() { ); } + // Block access for customers and employees + if (!hasAccess) { + return ( +
+
+ + + +

Access Denied

+

+ This page is only accessible to admins and super admins. +

+
+
+ ); + } + return (
diff --git a/src/app/dashboard/admin/page.tsx b/src/app/dashboard/admin/page.tsx index 89a2ef4..83f1ade 100644 --- a/src/app/dashboard/admin/page.tsx +++ b/src/app/dashboard/admin/page.tsx @@ -4,8 +4,10 @@ import { useState, useEffect } from 'react'; import Link from 'next/link'; import { adminService } from '@/services/adminService'; import { AnalyticsDashboardResponse } from '@/types/admin'; +import { useDashboard } from '@/app/contexts/DashboardContext'; export default function AdminPage() { + const { roles, loading: rolesLoading } = useDashboard(); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); @@ -25,7 +27,11 @@ export default function AdminPage() { } }; - if (loading) { + // Check if user has permission to access this page + const hasRole = (role: string) => roles?.includes(role); + const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); + + if (rolesLoading || loading) { return (
@@ -40,6 +46,28 @@ export default function AdminPage() { ); } + // Block access for customers and employees + if (!hasAccess) { + return ( +
+
+ + + +

Access Denied

+

+ This page is only accessible to admins and super admins. +

+
+
+ ); + } + return (
diff --git a/src/app/dashboard/admin/reports/page.tsx b/src/app/dashboard/admin/reports/page.tsx index 1d9a6b6..4aab25f 100644 --- a/src/app/dashboard/admin/reports/page.tsx +++ b/src/app/dashboard/admin/reports/page.tsx @@ -3,20 +3,24 @@ import { useState } from 'react'; import { adminService } from '@/services/adminService'; import { ReportRequest } from '@/types/admin'; +import { useDashboard } from '@/app/contexts/DashboardContext'; export default function ReportsPage() { + const { roles, loading: rolesLoading } = useDashboard(); const [generating, setGenerating] = useState(false); - const [reportType, setReportType] = useState<'REVENUE' | 'SERVICES' | 'CUSTOMERS' | 'EMPLOYEES'>('REVENUE'); + const [reportType, setReportType] = useState<'SERVICE_PERFORMANCE' | 'REVENUE' | 'EMPLOYEE_PRODUCTIVITY' | 'CUSTOMER_SATISFACTION' | 'INVENTORY' | 'APPOINTMENT_SUMMARY'>('REVENUE'); + const [format, setFormat] = useState<'JSON' | 'PDF' | 'EXCEL' | 'CSV'>('PDF'); const [dateRange, setDateRange] = useState({ - from: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], - to: new Date().toISOString().split('T')[0], + fromDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], + toDate: new Date().toISOString().split('T')[0], }); const handleGenerateReport = async () => { try { setGenerating(true); const request: ReportRequest = { - reportType, + type: reportType, + format, ...dateRange, }; const report = await adminService.generateReport(request); @@ -31,6 +35,43 @@ export default function ReportsPage() { } }; + // Check if user has permission to access this page + const hasRole = (role: string) => roles?.includes(role); + const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); + + if (rolesLoading) { + return ( +
+
+
+
+
+
+ ); + } + + // Block access for customers and employees + if (!hasAccess) { + return ( +
+
+ + + +

Access Denied

+

+ This page is only accessible to admins and super admins. +

+
+
+ ); + } + return (
@@ -46,13 +87,29 @@ export default function ReportsPage() { +
+ +
+ +
@@ -61,8 +118,8 @@ export default function ReportsPage() { setDateRange({ ...dateRange, from: e.target.value })} + value={dateRange.fromDate} + onChange={(e) => setDateRange({ ...dateRange, fromDate: e.target.value })} className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 theme-text-primary focus:ring-2 focus:ring-blue-500" />
@@ -70,8 +127,8 @@ export default function ReportsPage() { setDateRange({ ...dateRange, to: e.target.value })} + value={dateRange.toDate} + onChange={(e) => setDateRange({ ...dateRange, toDate: e.target.value })} className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 theme-text-primary focus:ring-2 focus:ring-blue-500" />
diff --git a/src/app/dashboard/admin/service-types/page.tsx b/src/app/dashboard/admin/service-types/page.tsx index 8e481ad..ca8ab4c 100644 --- a/src/app/dashboard/admin/service-types/page.tsx +++ b/src/app/dashboard/admin/service-types/page.tsx @@ -2,11 +2,22 @@ import { useState, useEffect } from 'react'; import { adminService } from '@/services/adminService'; -import { ServiceTypeResponse } from '@/types/admin'; +import { ServiceTypeResponse, CreateServiceTypeRequest, UpdateServiceTypeRequest } from '@/types/admin'; +import { useDashboard } from '@/app/contexts/DashboardContext'; export default function ServiceTypesPage() { + const { roles, loading: rolesLoading } = useDashboard(); const [serviceTypes, setServiceTypes] = useState([]); const [loading, setLoading] = useState(true); + const [showModal, setShowModal] = useState(false); + const [editingService, setEditingService] = useState(null); + const [formData, setFormData] = useState({ + name: '', + category: 'MAINTENANCE', + description: '', + price: '', + durationMinutes: 30, + }); useEffect(() => { loadServiceTypes(); @@ -16,17 +27,132 @@ export default function ServiceTypesPage() { try { setLoading(true); const data = await adminService.getAllServiceTypes(); - // Ensure data is an array - setServiceTypes(Array.isArray(data) ? data : []); + console.log('Loaded service types:', data); // Debug log + // Ensure we always get an array and include both active and inactive services + const services = Array.isArray(data) ? data : []; + setServiceTypes(services); + console.log('Set service types count:', services.length); // Debug log } catch (err) { console.error('Failed to load service types:', err); - setServiceTypes([]); // Set empty array on error + setServiceTypes([]); } finally { setLoading(false); } }; - if (loading) { + const handleCreate = () => { + setEditingService(null); + setFormData({ + name: '', + category: 'MAINTENANCE', + description: '', + price: '', + durationMinutes: 30, + }); + setShowModal(true); + }; + + const handleEdit = (service: ServiceTypeResponse) => { + setEditingService(service); + setFormData({ + name: service.name, + category: service.category, + description: service.description || '', + price: service.basePriceLKR.toString(), + durationMinutes: service.estimatedDurationMinutes, + }); + setShowModal(true); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + if (editingService) { + // Update existing service - use UpdateServiceTypeRequest format + console.log('Updating service:', editingService.id); + const updateData: UpdateServiceTypeRequest = { + name: formData.name, + category: formData.category, + description: formData.description, + price: parseFloat(formData.price) || 0, + durationMinutes: formData.durationMinutes, + active: editingService.active, // Keep existing active status + }; + await adminService.updateServiceType(editingService.id, updateData); + alert('Service type updated successfully!'); + } else { + // Create new service - use CreateServiceTypeRequest format + console.log('Creating service:', formData); + const createData: CreateServiceTypeRequest = { + name: formData.name, + category: formData.category, + description: formData.description, + price: parseFloat(formData.price) || 0, + durationMinutes: formData.durationMinutes, + }; + await adminService.createServiceType(createData); + alert('Service type created successfully!'); + } + + // Close modal first + setShowModal(false); + setEditingService(null); + + // Then reload the list + console.log('Reloading service types...'); + await loadServiceTypes(); + console.log('Service types reloaded'); + } catch (err) { + console.error('Failed to save service type:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to save service type. Please try again.'; + alert(errorMessage); + } + }; + + const handleDelete = async (service: ServiceTypeResponse) => { + if (!confirm(`Are you sure you want to delete "${service.name}"? This action cannot be undone.`)) { + return; + } + + try { + await adminService.removeServiceType(service.id); + alert('Service type deleted successfully!'); + await loadServiceTypes(); + } catch (err) { + console.error('Failed to delete service type:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to delete service type. Please try again.'; + alert(errorMessage); + } + }; + + const handleToggleActive = async (service: ServiceTypeResponse) => { + try { + const newStatus = !service.active; + console.log(`Toggling service ${service.id} from ${service.active} to ${newStatus}`); + + await adminService.updateServiceType(service.id, { + name: service.name, + category: service.category, + description: service.description || '', + price: service.basePriceLKR, + durationMinutes: service.estimatedDurationMinutes, + active: newStatus, + }); + + console.log('Toggle successful, reloading list...'); + await loadServiceTypes(); + } catch (err) { + console.error('Failed to toggle service status:', err); + alert('Failed to update service status. Please try again.'); + } + }; + + // Check if user has permission to access this page + const hasRole = (role: string) => roles?.includes(role); + const hasAccess = hasRole('ADMIN') || hasRole('SUPER_ADMIN'); + + if (rolesLoading || loading) { return (
@@ -38,21 +164,59 @@ export default function ServiceTypesPage() { ); } + // Block access for customers and employees + if (!hasAccess) { + return ( +
+
+ + + +

Access Denied

+

+ This page is only accessible to admins and super admins. +

+
+
+ ); + } + return (
-
-

Service Types

-

Manage available service types and pricing

+
+
+

Service Types

+

Manage available service types and pricing

+
+
{serviceTypes.length === 0 ? (
-

No service types found

+

No service types found. Create one to get started!

) : ( serviceTypes.map((serviceType) => ( -
+

{serviceType.name}

{serviceType.active ? ( @@ -60,13 +224,13 @@ export default function ServiceTypesPage() { Active ) : ( - + Inactive )}

{serviceType.description}

-
+
Category: {serviceType.category} @@ -82,10 +246,155 @@ export default function ServiceTypesPage() { {serviceType.estimatedDurationMinutes ?? 0} min
+ + {/* Action Buttons */} +
+ + + +
)) )}
+ + {/* Create/Edit Modal */} + {showModal && ( +
+
+
+
+

+ {editingService ? 'Edit Service Type' : 'Create Service Type'} +

+ +
+ + +
+ + setFormData({ ...formData, name: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 theme-text-primary" + placeholder="e.g., Oil Change" + /> +
+ +
+ + +

Choose the service category

+
+ +
+ +
- No audit logs found + + + + +

+ {error ? 'Unable to load audit logs' : 'No audit logs found'} +

+

+ Audit logs will appear here as system activities are tracked +